/* eslint-disable max-len */
/* eslint-disable no-bitwise */
/**
 * 项目版本模型
 *
 * Created by snowingsea on 2020/02/07.
 */
const equal = require('deep-equal');
const fs = require('fs-extra');
const moment = require('moment');
const mongoose = require('mongoose');
const path = require('path');
const escapeRegexp = require('escape-regexp');
const { v4: uuidv4 } = require('uuid');
const _ = require('underscore');
const randomstring = require('randomstring');
const ProjectVersion = require('./mongo/project-version');
const Project = require('./mongo/project');
const File = require('./mongo/file');
const Filter = require('./mongo/filter');
const files = require('./files');
const functions = require('./functions');
const codeDefects = require('./code-defects');
const tests = require('./tests');
const tasks = require('./tasks');
const defects = require('./defects');
const gitClient = require('./git/git-client');
const gitServer = require('./git/git-sever');
const publisher = require('./redis/publisher');
const common = require('./common');
const utility = require('./utility');
const machine = require('./machine');
const numbers = require('./numbers');
const Test = require('./mongo/test-case');
const FileGlobalVariable = require('./mongo/file-global-variable');
const FunctionVariable = require('./mongo/function-variable');
const Function = require('./mongo/function');
const PhoenixError = require('./phoenix-error');
const fileElements = require('./file-elements');
const TestCaseFieldConfig = require('./mongo/test-case-field-config');
const functionRelations = require('./function-relations');
const invokeRelations = require('./invoke-relations');
const { eventHub, eventKind } = require('./event-hub');
const UserDefinedFields = require('./mongo/user-defined-fields');
const cacheUtil = require('../cacheUtil');
const Excel = require('./export/report/adapter/excel');
const licenses = require('./license');
/**
 * 项目状态
 * @type {Object}
 */
const versionStatusKind = {
  deleting: -1, // 正在删除
  initial: 0, // 初始化状态
  normal: 1, // 正常状态
  frozen: 10, // 版本冻结
};
// 收集覆盖率范围
const collectCoverageKind = {
  all: 1, // 收集整个版本
  partial: 2, // 收集部分数据
};

// 版本类型
const versionType = {
  unitTest: 'unitTest', // 单元测试
  staticAnalyze: 'staticAnalyze', // 静态分析
  integrationTest: 'integrationTest', // 集成测试
};
// 版本配置
const versionConfigFields = {
  // 主版本和子版本公用字段
  commonFields: [
    'versionName',
    'environmentId',
    'environmentChanged',
    'projectType',
    'language',
    'environment',
    'systems',
    'languages',
    'headers',
    'includes',
    'macros',
    'ignoreAsm',
    'caseSensitive',
    'systemType',
    'exits',
    'repository',
    'visitAt',
    'createAt',
    'finishAt',
    'oldVersionId',
    'includeFiles',
    'arguments',
    'integrateStdLib',
    'exits',
    'environmentMacrosShow',
    'environmentArgumentsShow',
    'properties',
    'encoding',
    'autoFill',
    'commandScriptBeforeParse',
    'commandScriptBeforeExecute',
    'commandScriptBeforeExport',
    'onlyReplaceStubFunction',
    'includedHeaders',
  ],
};
/**
 * 回归版本弹出状态
 * @type {Object}
 */
const regressionStatusPrompt = {
  none: 0, // 不做任何操作
  removeOriginalVersion: 1, // 老版本被删除
  modifyOriginalVersion: 2, // 老版本被修改
};
/**
 * 系统类型
 * @type {Object}
 */
const systemType = {
  unassigned: 0, // 未指定
  bit32: 32, // 32位系统模式
  bit64: 64, // 64位系统模式
};

const libKind = {
  standard: ['std', '__gnu_cxx'],
  qt: ['qt'],
};

// 追溯矩阵的常规列类型
const traceabilityMatrixCommonColumn = {
  fileName: 'fileName',
  functionName: 'functionName',
  testIdentification: 'testIdentification',
  description: 'description',
  requirementInfo: 'requirementInfo',
  typeNumber: 'typeNumber',
};

/**
 * 获取概要json信息
 * @param {ProjectVersion} projectVersion 项目版本
 * @returns {Object}
 */
function getSummaryJson(projectVersion) {
  const versionId = projectVersion._id; // eslint-disable-line no-underscore-dangle
  const json = {
    versionId,
    versionName: projectVersion.versionName,
    status: projectVersion.status,
    createAt: versionId.getTimestamp().getTime(),
    visitAt: projectVersion.visitAt.getTime(),
    coverages: projectVersion.coverages,
    mcdcKind: projectVersion.mcdcKind || tests.mcdcKind.masking,
    collectCoverageKind: projectVersion.collectCoverageKind || collectCoverageKind.all,
    encoding: projectVersion.encoding || 'utf-8',
    onlyReplaceStubFunction: projectVersion.onlyReplaceStubFunction || false,
    builtIn: projectVersion.builtIn,
    language: projectVersion.language,
    systems: {},
  };
  if (projectVersion.rootDirectory) {
    let suffixes = [];
    Object.keys(projectVersion.languages).forEach((key) => {
      suffixes = suffixes.concat(projectVersion.languages[key].suffixes);
    });
    const extensions = suffixes.concat(projectVersion.headers.suffixes);
    json.rootDirectory = files.getSummaryJson(projectVersion.rootDirectory, extensions);
  }
  if (projectVersion.finishAt) {
    json.finishAt = projectVersion.finishAt.getTime();
  }
  if (projectVersion.oldVersionId) {
    json.oldVersionId = projectVersion.oldVersionId;
  }
  if (projectVersion.versionType) {
    json.versionType = projectVersion.versionType;
  }
  if (projectVersion.masterVersionId) {
    json.masterVersionId = projectVersion.masterVersionId;
  }
  // 兼容老项目的编译环境结构
  if (projectVersion.system
    && Object.keys(projectVersion.system).length > 0
    && projectVersion.environmentId
    && projectVersion.environmentId.length === 1) {
    json.systems = {
      [projectVersion.environmentId[0].toString()]: projectVersion.system,
    };
  }
  if (projectVersion.systems) {
    Object.assign(json.systems, projectVersion.systems);
  }
  if (!utility.isEmpty(projectVersion.oldVersion)) {
    json.oldVersionName = projectVersion.oldVersion[0].versionName;
  }
  if (projectVersion.commandScriptBeforeParse) {
    json.commandScriptBeforeParse = projectVersion.commandScriptBeforeParse;
  }
  if (projectVersion.commandScriptBeforeExecute) {
    json.commandScriptBeforeExecute = projectVersion.commandScriptBeforeExecute;
  }
  if (projectVersion.commandScriptBeforeExport) {
    json.commandScriptBeforeExport = projectVersion.commandScriptBeforeExport;
  }

  if (projectVersion.includedHeaders) {
    json.includedHeaders = projectVersion.includedHeaders;
  }
  return json;
}

/**
 * 获取json信息
 * @param {ProjectVersion} projectVersion 项目版本
 * @returns {Object}
 */
function getJson(projectVersion) {
  const json = getSummaryJson(projectVersion);
  json.description = projectVersion.description;
  json.environmentId = projectVersion.environmentId;
  json.environmentChanged = projectVersion.environmentChanged;
  json.environmentMacrosShow = projectVersion.environmentMacrosShow;
  json.environmentArgumentsShow = projectVersion.environmentArgumentsShow;
  json.headers = projectVersion.headers;
  json.languages = projectVersion.languages;
  json.includes = projectVersion.includes;
  json.includeFiles = projectVersion['include files'];
  json.macros = projectVersion.macros.map(({ key, value }) => ({ key, value }));
  json.arguments = projectVersion.arguments;
  json.timeout = projectVersion.timeout;
  json.timeoutTestGen = projectVersion.timeoutTestGen;
  json.timeoutTestRun = projectVersion.timeoutTestRun;
  json.extraExit = projectVersion.extraExit;
  json.caseSensitive = projectVersion.caseSensitive;
  json.systemType = projectVersion.systemType;
  json.strictOutputCheck = projectVersion.strictOutputCheck;
  json.repository = projectVersion.repository;
  json.language = projectVersion.language;
  json.environment = projectVersion.environment;
  json.ignoreAsm = projectVersion.ignoreAsm;
  json.projectType = projectVersion.projectType;
  json.exits = projectVersion.exits || [];
  json.analyzeTemplate = projectVersion.analyzeTemplate || '';
  json.integrateStdLib = false;
  json.integrateQtLib = false;
  json.versionType = projectVersion.versionType;
  json.masterVersionId = projectVersion.masterVersionId;
  json.testIdentifierRule = projectVersion.testIdentifierRule || {};
  json.properties = projectVersion.properties || {};
  json.autoNullCheck = projectVersion.autoNullCheck || false;
  json.autoFill = projectVersion.autoFill || false;
  json.commandScriptBeforeParse = projectVersion.commandScriptBeforeParse || '';
  json.commandScriptBeforeExecute = projectVersion.commandScriptBeforeExecute || '';
  json.commandScriptBeforeExport = projectVersion.commandScriptBeforeExport || '';
  json.onlyReplaceStubFunction = projectVersion.onlyReplaceStubFunction || false;
  json.includedHeaders = projectVersion.includedHeaders || '';

  const integrateLibs = {
    integrateStdLib: 'standard',
    integrateQtLib: 'qt',
  };
  Object.keys(integrateLibs).forEach((key) => {
    if (projectVersion.excludeNamespaces
      && libKind[integrateLibs[key]].every(
        (namespace) => projectVersion.excludeNamespaces.indexOf(namespace) >= 0
      )
    ) {
      json[key] = true;
    }
  });

  Object.keys(projectVersion.eventReminder).forEach((key) => {
    if (key === '$init') {
      return;
    }
    json[`eventReminder.${key}`] = projectVersion.eventReminder[key];
  });
  if (!json.environmentId) {
    delete json.environmentId;
  }
  return json;
}

/**
 * 获取项目版本键值
 * @param {String} projectId 项目id
 * @param {String} versionName 项目版本名称
 * @return {String}
 */
function getVersionKey(projectId, versionName) {
  return `${projectId}:${versionName}`;
}
/**
 * 获取版本信息
 * @param {String} versionId 版本id
 * @return {Promise}
 */
function getVersion(versionId) {
  return new Promise((resolve, reject) => {
    let version;
    ProjectVersion.findOne({ _id: mongoose.Types.ObjectId(versionId) })
      .then((res) => {
        version = res;
        if (!res || !res.rootDirectoryId) {
          return Promise.resolve();
        }
        return files.getFile(version.rootDirectoryId);
      })
      .then(async (file) => {
        if (version) {
          version.dataDepth = version.dataDepth || 6;
          version.rootDirectory = file;
          version.projectType = version['project type'];
          delete version['project type'];
          version.ignoreAsm = version['ignore asm'];
          delete version['ignore asm'];
          version.excludeNamespaces = version['exclude namespaces'];
          delete version['exclude namespaces'];
        }
        resolve(version);
      })
      .catch(reject);
  });
}

/**
 * 获取版本头文件源文件后缀
 * @param {String} versionId 版本id
 * @returns {Promise}
 */
function getVersionSuffixes(versionId) {
  return new Promise((resolve, reject) => {
    let suffixes = [];
    getVersion(versionId)
      .then((version) => {
        // 头文件后缀
        let headersSuffix = [];
        if (version.headers) {
          headersSuffix = version.headers.suffixes ? version.headers.suffixes : [];
        }
        let languagesSuffix = [];
        // 源文件后缀
        if (version.languages) {
          Object.values(version.languages).forEach((languageItem) => {
            if (languageItem.suffixes) {
              languagesSuffix = languagesSuffix.concat(languageItem.suffixes);
            }
          });
        }
        suffixes = headersSuffix.concat(languagesSuffix);
        resolve(suffixes);
      })
      .catch(reject);
  });
}

/**
 * 获取最近项目版本
 * @param {String} creatorId 创建者id
 * @param {String} projectId 项目id
 * @return {Promise}
 */
function getLastCreatedVersion({ creatorId = null, projectId = null }) {
  return new Promise((resolve, reject) => {
    let version;
    const conditions = {
      status: { $ne: versionStatusKind.deleting },
      versionType: versionType.unitTest,
    };
    if (projectId) {
      projectId = mongoose.Types.ObjectId(projectId);
      conditions.projectId = projectId;
    }
    if (creatorId) {
      creatorId = mongoose.Types.ObjectId(creatorId);
      conditions.creatorId = creatorId;
    }
    ProjectVersion.find(conditions).sort({ _id: -1 }).limit(1).exec()
      .then(([res]) => {
        version = res;
        if (!res || !res.rootDirectoryId) {
          return Promise.resolve();
        }
        return files.getFile(version.rootDirectoryId);
      })
      .then((file) => {
        if (version) {
          version.rootDirectory = file;
        }
        resolve(version);
      })
      .catch(reject);
  });
}

/**
 * 更新版本信息文件
 * @param {String} versionId 当前项目版本文件id
 * @param {String} content 文件内容
 * @param {ProjectVersion} version 项目版本
 * @param {Project} project 项目
 * @param {User} creator 创建者
 */
function updateVersionFile(
  versionId,
  version = null,
  project = null,
  creator = null // eslint-disable-line comma-dangle
) {
  return new Promise((resolve, reject) => {
    common.complementByProjectVersionId(versionId, version, project, creator)
      .then((res) => {
        version = res.version;
        project = res.project;
        creator = res.creator;
        return Promise.resolve();
      })
      .then(() => files.getFileByPath(versionId, files.VERSIONFIELNAME))
      .then((file) => {
        // eslint-disable-next-line no-underscore-dangle
        const fileId = file._id;
        return files.updateFile(
          fileId,
          { content: JSON.stringify(version) },
          file,
          version,
          project,
          creator,
        );
      })
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 补全根目录信息
 * @param {Array} versions 版本列表
 * @return {Promise}
 */
function complementRootDirectory(versions) {
  return new Promise((resolve, reject) => {
    const rootDirectoryIds = versions.map(({ rootDirectoryId }) => rootDirectoryId);
    File.find({ _id: { $in: rootDirectoryIds } })
      .then((res) => {
        const rootDirectories = new Map();
        res.forEach((file) => {
          const fileId = file._id.toString(); // eslint-disable-line no-underscore-dangle
          rootDirectories.set(fileId, file);
        });
        versions.forEach((version) => {
          if (version.rootDirectoryId) {
            version.rootDirectory = rootDirectories.get(version.rootDirectoryId.toString());
          }
        });
        resolve(versions);
      })
      .catch(reject);
  });
}

/**
 * 获取版本集合
 * @param {Array} versionIds 版本id列表
 * @param {Array} exceptVersionIds 不计入的版本id列表
 * @param {Object} options 选项
 * @return {Promise}
 */
function getVersions({ versionIds = null, exceptVersionIds = null }, options = {}) {
  return new Promise((resolve, reject) => {
    if (versionIds) {
      if (!utility.isArrayType(versionIds)) {
        versionIds = [versionIds];
      }
      versionIds = versionIds.map((versionId) => mongoose.Types.ObjectId(versionId));
      options._id = { $in: versionIds }; // eslint-disable-line no-underscore-dangle
    } else {
      if (!exceptVersionIds) {
        exceptVersionIds = [];
      }
      if (!utility.isArrayType(exceptVersionIds)) {
        exceptVersionIds = [exceptVersionIds];
      }
      exceptVersionIds = exceptVersionIds.map((versionId) => mongoose.Types.ObjectId(versionId));
      options._id = { $nin: exceptVersionIds }; // eslint-disable-line no-underscore-dangle
    }
    ProjectVersion.find(options)
      .then((versions) => complementRootDirectory(versions))
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 获取项目版本列表请求约束
 * @param {Array|Number} status 项目版本状态
 * @param {String} q 搜索关键词
 * @return {Object}
 */
function getVersionOptions({ status = null, q = null }) {
  const options = {};
  // 项目版本状态
  if (status) {
    if (!utility.isArrayType(status)) {
      status = [status];
    }
    options.status = { $in: status };
  }
  // 关键词搜索
  if (q) {
    options.versionName = { $regex: `.*${escapeRegexp(q)}.*` };
  }
  return options;
}

/**
 * 筛选项目版本列表
 * @param {Object} pagination 分页信息
 * @param {Object} options 用户筛选项
 * @return {Promise}
 */
function filter(pagination = {}, options = {}) {
  return new Promise((resolve, reject) => {
    const { sortBy } = pagination;
    if (!sortBy || (sortBy === 'versionId') || (sortBy === 'createAt')) {
      pagination.sortBy = '_id';
    }
    pagination.useAggregate = true;
    Filter.filter(ProjectVersion, pagination, options, {
      $lookup: {
        from: 'projectversions', localField: 'oldVersionId', foreignField: '_id', as: 'oldVersion',
      },
    })
      .then((versions) => complementRootDirectory(versions))
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 项目版本数目
 * @param {Object} options 用户筛选项
 * @return {Promise}
 */
function getCount(options = {}) {
  return ProjectVersion.countDocuments(options);
}

/**
 * 添加项目版本
 * @param {String} projectId 项目id
 * @param {Object} doc 版本信息
 * @param {Project} project 项目
 * @param {User} creator 创建者
 * @param {License} license 证书
 * @param {boolean} isEnvironment 是否是编译环境
 * @return {Promise}
 */
function addVersion(projectId, doc, project = undefined, creator = undefined, license = undefined, isEnvironment = false) {
  return new Promise((resolve, reject) => {
    projectId = mongoose.Types.ObjectId(projectId);
    let version;
    let versionId;
    let creatorId;
    let rootDirectory;
    doc.projectId = projectId;
    doc.key = getVersionKey(projectId, doc.versionName);
    doc.visitAt = moment();
    doc.version = global.config.configurationVersion;
    if (doc.exits && !utility.isArrayType(doc.exits)) {
      doc.exits = [doc.exits];
    }
    if (license && licenses.isEducationVersion(license)) {
      doc.mcdcKind = tests.mcdcKind.condition;
    }
    const updateInfo = { status: versionStatusKind.normal };
    common.complementByProjectId(projectId, project, creator)
      .then((res) => {
        project = res.project;
        creator = res.creator;

        const { coverages } = doc;
        if (coverages) {
          doc.coverages = 0;
          if (utility.isArrayType(coverages)) {
            coverages.forEach((coverage) => {
              coverage = Number.parseInt(coverage.toString(), 10);
              doc.coverages |= coverage; // eslint-disable-line no-bitwise
            });
          } else {
            doc.coverages = Number.parseInt(coverages.toString(), 10);
          }
        } else {
          doc.coverages = tests.coverageKind.statement
            | tests.coverageKind.branch
            | tests.coverageKind.mcdc;
        }
        creatorId = creator._id;
        doc.creatorId = creatorId;
        // eslint-disable-next-line no-use-before-define
        return getLastSelectedTemplate(creatorId);
      })
      .then((lastSelectTemplate) => {
        const { analyzeTemplate } = doc;
        if (analyzeTemplate) {
          doc.analyzeTemplate = mongoose.Types.ObjectId(analyzeTemplate);
        } else if (lastSelectTemplate) {
          doc.analyzeTemplate = lastSelectTemplate;
        }
        return machine.getCompilationOptions();
      })
      .then((options) => {
        const {
          projectType,
          ignoreAsm,
          system,
          environment,
          languages,
          headers,
          integrateStdLib,
          integrateQtLib,
        } = doc;
        if (!system) {
          doc.system = options['project types'][projectType].system;
        }
        if (!environment) {
          doc.environment = options['project types'][projectType].environment;
        }
        const { language } = doc;
        if (!languages) {
          doc.languages = {};
          const newLanguages = [language];
          if (!['java', 'python'].includes(language)) {
            newLanguages.push('c');
          }
          newLanguages.forEach((lang) => {
            const standard = options['project types'][projectType].languages[lang].standards[0];
            doc.languages[lang] = {
              standard,
              suffixes: options.languages[lang].suffixes,
            };
          });
        }
        if (!headers) {
          doc.headers = { suffixes: options.languages[language].headers };
        }
        doc['project type'] = projectType;
        delete doc.projectType;
        doc['ignore asm'] = ignoreAsm;
        delete doc.ignoreAsm;
        let excludeNamespaces = [];
        if (integrateStdLib === true) {
          excludeNamespaces = excludeNamespaces.concat(libKind.standard);
        }
        if (integrateQtLib === true) {
          excludeNamespaces = excludeNamespaces.concat(libKind.qt);
        }
        doc['exclude namespaces'] = excludeNamespaces;
        delete doc.integrateStdLib;
        delete doc.integrateQtLib;
        const version = new ProjectVersion(doc);
        return version.save();
      })
      .then(async (res) => {
        version = res;
        versionId = version._id;
        version.repository = version.repository || {};
        version.repository.url = gitServer.getRemoteURL(creator, project);
        // 编译环境每一个版本使用一个独立的仓库
        if (isEnvironment) {
          await gitClient.initRepo(project, version);
          version.repository.url = gitServer.getRemoteURL(project, version);
        }
        updateInfo.repository = version.repository;
        await gitClient.createBranch(creator, project, version);
        updateInfo.currentCommitId = await gitClient.getLatestCommitId(creator, project, version);
        rootDirectory = await files.addDirectory(creatorId, projectId, versionId, '.');
        updateInfo.rootDirectoryId = rootDirectory._id;
        try {
          await ProjectVersion.updateOne({ _id: versionId }, updateInfo);
        } catch (err) {
          await ProjectVersion.deleteOne({ _id: versionId });
          throw err;
        }
        await Project.updateOne({ _id: projectId }, { lastVersionId: versionId });
        return getVersion(versionId);
      })
      .then((resVersion) => {
        version = resVersion;
        const kind = files.kind.file;
        const fileName = files.VERSIONFIELNAME;
        const content = JSON.stringify(version);
        return files.addFile2Directory(
          updateInfo.rootDirectoryId,
          { kind, fileName, content },
          rootDirectory,
          version,
          project,
          creator,
        );
      })
      .then(() => {
        const data = getJson(version);
        data.projectId = projectId;
        data.creatorId = project.creatorId;
        publisher.publishProjectVersionMessage('add', data);
        resolve(version);
      })
      .catch(reject);
  });
}

async function cloneFiles(masterVersionId, subVersionId) {
  const subVersion = await ProjectVersion.findById(mongoose.Types.ObjectId(subVersionId));
  if (subVersion.rootDirectoryId) {
    return;
  }
  const rootDirectoryId = await files.cloneFilesFromMasterVersion(masterVersionId, subVersionId);
  const { currentCommitId } = await ProjectVersion.findById(mongoose.Types.ObjectId(masterVersionId));
  await ProjectVersion.updateOne({ _id: mongoose.Types.ObjectId(subVersionId) }, { rootDirectoryId, currentCommitId });
}

async function initTestIdentifierNumber(versionId) {
  const tests = await Test.find({ versionId: mongoose.Types.ObjectId(versionId) }, ['_id']).sort({ functionId: 1, number: 1 });
  await Test.bulkWrite(
    tests.map((test, index) => ({
      updateOne: {
        filter: { _id: test._id },
        update: { $set: { identifierNumber: index + 1 } },
      },
    })),
  );
  await numbers.restore(numbers.getTestcaseIdentifierNumberName(versionId), tests.length);
}

/**
 * 获取当前版本的关联版本
 * @param {string} versionId
 */
async function getRelationVersions(versionId, {
  includeSelf = false,
} = {}) {
  let relationVersions = [];
  const version = await ProjectVersion.findById(mongoose.Types.ObjectId(versionId));
  if (!version.masterVersionId) { // 当前版本是主版本
    relationVersions = await ProjectVersion.find({
      masterVersionId: mongoose.Types.ObjectId(versionId),
    });
  } else { // 当前版本是子版本
    relationVersions = await ProjectVersion.find({
      $or: [
        {
          masterVersionId: mongoose.Types.ObjectId(version.masterVersionId),
        }, {
          _id: mongoose.Types.ObjectId(version.masterVersionId),
        }],
      _id: {
        $nin: [mongoose.Types.ObjectId(versionId)],
      },
    });
  }

  return includeSelf ? [...relationVersions, version] : relationVersions;
}
async function getSmartRocketFormatContent(versionId) {
  let originalSystem = null;
  const environmentMap = new Map();
  const doc = {
    version: '2.6.0',
    unitTest: {},
    staticAnalyze: {},
    integrationTest: {},

  };
  versionId = mongoose.Types.ObjectId(versionId);
  const version = await ProjectVersion.findById(versionId);
  if (!version || version.status === versionStatusKind.deleting) {
    return null;
  }
  if (version.environmentId) {
    (await ProjectVersion.find({ _id: { $in: version.environmentId } })).forEach((env) => {
      environmentMap.set(env._id.toString(), env);
    });
  }
  // 处理老项目编译环境配置
  if (version.system && version.environmentId && version.environmentId.length === 1) {
    originalSystem = { [version.environmentId[0].toString()]: { includes: version.system.includes } };
  }
  const versions = await getRelationVersions(versionId, { includeSelf: true });
  const promises = [];
  versions.forEach((subVersion) => {
    // 更新老项目编译环境配置
    if (originalSystem) {
      promises.push(ProjectVersion.updateOne({ _id: subVersion._id },
        {
          $set: { systems: originalSystem, environmentId: subVersion.environmentId },
          $unset: { system: 1 },
        }));
      subVersion.systems = subVersion.systems ? { ...originalSystem, ...subVersion.systems } : originalSystem;
    }
    if (subVersion.versionType === versionType.unitTest) {
      // 获取通用配置和单元测试配置
      [
        'includes',
        'include files',
        'arguments',
        'caseSensitive',
        'systemType',
        'autoFill',
        'strictOutputCheck',
        'ignore asm',
        'exits',
        'exclude namespaces',
        'language',
        'macros',
        'environment',
        'languages',
        'headers',
        'project type',
        'properties',
        'commandScriptBeforeParse',
        'commandScriptBeforeExecute',
        'commandScriptBeforeExport',
        'onlyReplaceStubFunction',
        'includedHeaders',
        'environmentId',
      ].forEach((key) => {
        if (subVersion[key] !== undefined) {
          doc[key] = subVersion[key];
        }
      });
      // 重新构建新的systems给到核心，同时兼容老项目system处理
      if (subVersion.environmentId && subVersion.environmentId.length > 0) {
        doc.systems = [];
        subVersion.environmentId.forEach((id) => {
          id = id.toString();
          const environment = environmentMap.get(id);
          if (!environment) {
            return;
          }
          let newSystem = {
            name: environment.versionName,
            url: environment.repository.url,
            commit: environment.currentCommitId,
          };
          if (subVersion.systems && subVersion.systems[id]) {
            newSystem = { ...newSystem, ...subVersion.systems[id] };
          }
          if (environment.builtIn) {
            newSystem.url = environment.systemUrl;
            newSystem['project types'] = environment.projectTypes;
            newSystem.macros = environment.macros ? environment.macros.map((item) => ({ key: item.key, value: item.value })) : [];
            newSystem.arguments = environment.arguments;
            newSystem.systems = environment.systems;
            newSystem.includes = newSystem.includes ? newSystem.includes.concat(environment.includes || []) : newSystem.includes || [];
            delete newSystem.commit;
          }
          doc.systems.push(newSystem);
        });
      }
      doc.unitTest.timeoutTestGen = subVersion.timeoutTestGen;
      doc.unitTest.timeoutTestRun = subVersion.timeoutTestRun;
      doc.unitTest.coverages = subVersion.coverages;
      doc.unitTest.autoNullCheck = subVersion.autoNullCheck;
    }
    if (subVersion.versionType === versionType.integrationTest) {
      doc.integrationTest.timeoutTestRun = subVersion.timeoutTestRun;
      doc.integrationTest.timeoutTestGen = subVersion.timeoutTestGen;
      doc.integrationTest.coverages = subVersion.coverages;
      doc.integrationTest.autoNullCheck = subVersion.autoNullCheck;
    }
  });
  await Promise.all(promises);
  return doc;
}
/**
 *更新.smartrocket文件
 * @param {String} versionId 版本id
 */
async function updateSmartRocketFile(versionId) {
  const smartrocketContent = await getSmartRocketFormatContent(versionId);
  const version = await ProjectVersion.findById(versionId);
  if (!version || version.status === versionStatusKind.deleting || version.builtIn) {
    return null;
  }
  const masterVersionId = version.masterVersionId || version._id;
  const file = await files.getFileByPath(masterVersionId, files.VERSIONFIELNAME);
  if (!file) {
    return Promise.resolve();
  }
  if (['gcc-ACoreOS', 'gcc-SylixOS'].includes(smartrocketContent['project type'])) {
    smartrocketContent['project type'] = 'gcc';
  }
  return files.updateFile(
    file._id,
    { content: JSON.stringify(smartrocketContent) },
    file
  );
}
async function addSubVersion(masterVersionId, doc) {
  if (!doc || !doc.versionType) {
    throw new PhoenixError('invalid request', 'need sub version type');
  }
  const masterVersion = await ProjectVersion.findById(mongoose.Types.ObjectId(masterVersionId)).lean();
  if (!masterVersion) {
    throw new PhoenixError('not found', `project version ${masterVersionId} not exist`);
  }
  if (masterVersion.masterVersionId) {
    throw new PhoenixError('invalid request', `${masterVersionId} is not a master version`);
  }
  if (doc.versionType === versionType.integrationTest) {
    // 集成测试默认选中覆盖准侧
    if (!doc.coverages) {
      doc.coverages = tests.coverageKind.invoke | tests.coverageKind.invokedFunc;
    }
    // 集成测试默认显示关联文件
    if (doc.fileRelation === undefined) {
      doc.fileRelation = true;
    }
    if (doc.stubDepth === undefined || Number(doc.stubDepth) < 2) {
      doc.stubDepth = invokeRelations.defaultStubDepth;
    }
  }
  const insertDoc = _.defaults({
    ...doc,
    masterVersionId: mongoose.Types.ObjectId(masterVersionId),
  }, _.omit(masterVersion, [
    // 去掉一些字段
    '_id',
    'key', 'rootDirectoryId', 'tasks', 'versionType', 'masterVersionId',
    'regressionStatus',
    'eventReminder',
  ]));
  const result = await ProjectVersion.findOneAndUpdate(
    {
      projectId: insertDoc.projectId,
      versionName: insertDoc.versionName,
      versionType: insertDoc.versionType,
    },
    insertDoc,
    { upsert: true, new: true, rawResult: true }
  );
  return result.value;
}
async function initSubVersion(_masterVersionId, _versionType) {
  const masterVersionId = mongoose.Types.ObjectId(_masterVersionId);
  // 兼容老数据: versionType为空即判定为unit test
  const versionTypeOpt = _versionType === versionType.unitTest ? { $in: [versionType.unitTest, null] } : _versionType;
  let exists = await ProjectVersion.findOne({
    $or: [
      { masterVersionId },
      { _id: masterVersionId, masterVersionId: null }, // 要找的version可能就是一个master version
    ],
    versionType: versionTypeOpt,
  });
  if (!exists) {
    exists = await addSubVersion(masterVersionId, { versionType: _versionType });
    await updateSmartRocketFile(masterVersionId);
  }
  if (!exists.rootDirectoryId) {
    await cloneFiles(masterVersionId, exists._id);
  }
  const test = await Test.findOne({ versionId: exists._id });
  if (!test || !test.identifierNumber) {
    await initTestIdentifierNumber(exists._id);
  }
  return exists;
}
/**
 * 更新静态分析回归版本状态
 * @param {object} resVersion 原始主版本信息
 * @param {number} type 弹出状态
 */
async function updateStaticAnalyzeRegressionPrompt(resVersion, type) {
  let masterVersionId = resVersion._id;
  if (resVersion.masterVersionId) {
    masterVersionId = resVersion.masterVersionId;
  }
  const { _id: versionId } = await initSubVersion(masterVersionId, versionType.staticAnalyze);
  const { regressionStatus, _id } = await ProjectVersion.findById(versionId);
  if (regressionStatus > 0) {
    await ProjectVersion.updateOne({ _id }, {
      $set: {
        'eventReminder.regressionStatusPrompt': type,
      },
    });
  }
  if (type === regressionStatusPrompt.removeOriginalVersion) {
    await ProjectVersion.updateOne({ _id }, {
      $set: {
        'eventReminder.firstVisit': type,
      },
    });
  }
}
/**
 * 回归版本状态变更
 * @param {string} versionId 原始版本id
 * @param {number} type  操作
 *  */
async function updateRegressionStatusPrompt(versionId, type) {
  let version;
  switch (type) {
    case 'remove':
      type = regressionStatusPrompt.removeOriginalVersion;
      break;
    case 'update':
      type = regressionStatusPrompt.modifyOriginalVersion;
      version = await ProjectVersion.findById(versionId);
      if (version.masterVersionId) {
        versionId = version.masterVersionId;
      }
      break;
    default:
      break;
  }
  const regressionVersions = await ProjectVersion.find({ oldVersionId: versionId });
  const promises = [];
  for (let i = 0; i < regressionVersions.length; i += 1) {
    const reVersion = regressionVersions[i];
    if (reVersion.masterVersionId && reVersion.versionType === versionType.staticAnalyze) {
      promises.push(updateStaticAnalyzeRegressionPrompt(reVersion, type));
    }
  }
  return Promise.all(promises);
}
/**
 * 处理版本测试用例全局数据深度
 * @param {Number} dataDepth 数据深度
 * @param {Boolean} dataDepthCoverAll 测试用例显示数据深度是否全部覆盖：true:是; false：否
 */
async function handleGlobalDataDepthOfVersion(versionId, dataDepth, dataDepthCoverAll) {
  versionId = mongoose.Types.ObjectId(versionId);
  if (!dataDepth) {
    return;
  }
  await ProjectVersion.updateOne({ _id: versionId }, { dataDepth });
  if (dataDepthCoverAll) {
    // 覆盖原先已经设置的函数
    await TestCaseFieldConfig.updateMany({ versionId }, { dataDepth });
  }
}
/**
 * 更新版本信息
 * @param {String} versionId 项目版本id
 * @param {String} versionName 版本名称
 * @param {String} description 描述
 * @param {Array[String|ObjectId]} environmentId 编译环境id
 * @param {Boolean} environmentChange 是否同步编译环境
 * @param {Object} headers c头文件后缀名列表
 * @param {Object} languages c文件后缀名列表
 * @param {Array} includes 包含目录列表
 * @param {Array} macros 宏开关列表
 * @param {String} args 其他配置参数
 * @param {Number} timeout 超时时间
 * @param {Number|Array} coverages 覆盖准则
 * @param {Boolean} caseSensitive 大小写敏感
 * @param {Number} systemType 系统类型
 * @param {Boolean} strictOutputCheck 预期值严格检查模式
 * @param {Number} status 版本状态
 * @param {String} language 项目类型
 * @param {string} projectType 编译器
 * @param {Object} projectTypes 编译环境对应的可选编译器
 * @param {String} systemUrl 系统编译环境url
 * @param {string} environment 运行环境
 * @param {Boolean} ignoreAsm 是否忽略汇编
 * @param {Boolean} autoNullCheck 自动空值检查
 * @param {Array|String} exits 退出函数
 * @param {String} analyzeTemplate 检测模板Id
 * @param {Boolean} integrateStdLib 设置是否集成标准库
 * @param {Boolean} integrateQtLib 设置是否集成QT库
 * @param {Boolean} fileRelation 集成测试函数调用是否显示关联文件
 * @param {Boolean} environmentMacrosShow 是否显示编译环境宏定义
 * @param {Boolean} environmentArgumentsShow 是否显示编译环境其他配置
 * @param {Object} testIdentifierRule 测试用例标识生成规则
 * @param {Object} properties 编译参数
 * @param {ProjectVersion} version 项目版本
 * @param {Project} project 项目
 * @param {Object} systems 编译环境配置项 {"21212122":{}}
 * @param {User} creator 创建者
 * @param {Number} dataDepth 测试用例数据显示深度
 * @param {Boolean} dataDepthCoverAll 测试用例显示数据深度是否全部覆盖：true:是; false：否
 * @param {String} encoding 编码格式：utf-8,gbk,gb18030
 * @param {Object} tolerance 614通过准则
 *         {"=":1} 默认相等；
 *         {"+":"","-":""} 范围值；
 *         {"%":"60"} 百分比；
 * @return {Promise}
 */
function updateVersion(
  versionId,
  {
    versionName = undefined,
    description = undefined,
    environmentId = undefined,
    environmentChanged = undefined,
    headers = undefined,
    languages = undefined,
    includes = undefined,
    macros = undefined,
    arguments: args = undefined,
    timeout = undefined,
    timeoutTestGen = undefined,
    timeoutTestRun = undefined,
    coverages = undefined,
    caseSensitive = undefined,
    systemType = undefined,
    systems = undefined,
    strictOutputCheck = undefined,
    status = undefined,
    language = undefined,
    projectType = undefined,
    projectTypes = undefined,
    systemUrl = undefined,
    environment = undefined,
    ignoreAsm = undefined,
    exits = undefined,
    analyzeTemplate = undefined,
    integrateStdLib = undefined,
    integrateQtLib = undefined,
    eventReminder = undefined,
    fileRelation = undefined,
    environmentMacrosShow = undefined,
    environmentArgumentsShow = undefined,
    testIdentifierRule = undefined,
    autoNullCheck = undefined,
    properties = undefined,
    dataDepth = undefined,
    dataDepthCoverAll = undefined,
    encoding = undefined,
    tolerance = undefined,
    mcdcKind = undefined,
    autoFill = undefined,
    stubDepth = undefined,
    commandScriptBeforeParse = undefined,
    commandScriptBeforeExecute = undefined,
    commandScriptBeforeExport = undefined,
    onlyReplaceStubFunction = undefined,
    includedHeaders = undefined,
    enableErrorGuess = undefined, // 是否生成错误推测用例
  },
  version = undefined,
  project = undefined,
  creator = undefined
) {
  return new Promise((resolve, reject) => {
    const doc = {};
    if (exits && !utility.isArrayType(exits)) {
      exits = [exits];
    }
    if (exits === '') { // 无终止函数情况
      exits = [];
    }
    if ((args || args === '') && !utility.isArrayType(args)) {
      args = [args];
    }
    const originalStubDepth = version.stubDepth;
    const randId = uuidv4();
    handleGlobalDataDepthOfVersion(versionId, dataDepth, dataDepthCoverAll)
      .then(() => common.complementByProjectVersionId(versionId, version, project, creator))
      .then((res) => {
        version = res.version;
        const originalEnvironmentIds = version.environmentId ? version.environmentId.map((id) => id.toString()) : [];
        // 编译环境弹出状态修改
        if (environmentChanged !== undefined && !equal(environmentChanged, version.environmentChanged)) {
          doc.environmentChanged = environmentChanged;
        }
        if (environmentId && !utility.isObjectValueEqual(environmentId, originalEnvironmentIds)) {
          doc.environmentId = environmentId;
          doc.environmentChanged = false;
          return Promise.all(environmentId.map((id) => getVersion(id)));
        }
        return Promise.resolve();
      })
      .then((envs) => {
        if (envs) {
          doc.systems = {};
          environmentId.forEach((id) => {
            id = id.toString();
            if (version.systems && version.systems[id]) {
              doc.systems = { [id]: version.systems[id] };
            }
          });
        }
        if (systems && !utility.isObjectValueEqual(systems, version.systems)) {
          doc.environmentChanged = false;
          doc.systems = systems;
        }
        if (environmentMacrosShow !== undefined && !equal(environmentMacrosShow,
          version.environmentMacrosShow)) {
          doc.environmentMacrosShow = environmentMacrosShow;
        }
        if (environmentArgumentsShow !== undefined && !equal(environmentArgumentsShow,
          version.environmentArgumentsShow)) {
          doc.environmentArgumentsShow = environmentArgumentsShow;
        }
        if (fileRelation !== undefined && !equal(fileRelation, version.fileRelation)) {
          doc.fileRelation = fileRelation;
        }
        if (autoFill !== undefined && !equal(autoFill, version.autoFill)) {
          doc.autoFill = autoFill;
        }
        // 全局通过准则
        if (!_.isUndefined(tolerance) && !_.isEqual(tolerance, version.tolerance)) {
          doc.tolerance = tolerance;
        }
        // 编译环境编译可选编译器
        if (!_.isUndefined(projectTypes) && !_.isEqual(projectTypes, version.projectTypes)) {
          doc.projectTypes = projectTypes;
        }
        // 系统默认编译环境url
        if (!_.isUndefined(systemUrl) && !_.isEqual(systemUrl, version.systemUrl)) {
          doc.systemUrl = systemUrl;
        }
        // update version  encoding
        if (encoding && !equal(encoding, version.encoding)) {
          doc.encoding = encoding;
        }
        // 全局通过准则
        if (!_.isUndefined(tolerance) && !_.isEqual(tolerance, version.tolerance)) {
          doc.tolerance = tolerance;
        }
        if (versionName && !equal(versionName, version.versionName)) {
          doc.versionName = versionName;
          doc.key = getVersionKey(version.projectId, versionName);
        }
        if ((description || description === '') && !equal(description, version.description)) {
          doc.description = description;
        }
        if (headers && !equal(headers, version.headers)) {
          doc.headers = headers;
        }
        // 测试用例数据深度设置
        if (dataDepth && !equal(dataDepth, version.dataDepth)) {
          doc.dataDepth = dataDepth;
        }
        if (languages && !equal(languages, version.languages)) {
          doc.languages = languages;
        }
        if (includes && !equal(includes, version.includes)) {
          doc.includes = includes;
          doc.tasks = {};
        }
        if (macros && !equal(macros, version.macros)) {
          doc.macros = macros;
          doc.tasks = {};
        }
        if (args && !equal(args, version.arguments)) {
          doc.arguments = args;
        }
        if (timeout && !equal(timeout, version.timeout)) {
          doc.timeout = timeout;
        }
        if (timeoutTestGen && !equal(timeoutTestGen, version.timeoutTestGen)) {
          doc.timeoutTestGen = timeoutTestGen;
        }
        // 自动空值检查
        if (autoNullCheck !== undefined && !equal(autoNullCheck, version.autoNullCheck)) {
          doc.autoNullCheck = autoNullCheck;
        }
        if (timeoutTestRun && !equal(timeoutTestRun, version.timeoutTestRun)) {
          doc.timeoutTestRun = timeoutTestRun;
        }
        if (language && !equal(language, version.language)) {
          doc.language = language;
        }
        if (projectType && !equal(projectType, version['project type'])) {
          doc['project type'] = projectType;
          // 切换编译器编译参数清空
          properties = {};
        }
        // 编译参数
        if (properties && !equal(properties, version.properties)) {
          doc.properties = properties;
        }
        if (environment && !equal(environment, version.environment)) {
          doc.environment = environment;
        }
        if ((ignoreAsm || ignoreAsm === false) && !equal(ignoreAsm, version['ignore asm'])) {
          doc['ignore asm'] = ignoreAsm;
        }
        if (exits && !equal(exits, version.exits)) {
          doc.exits = exits;
        }
        if (mcdcKind && !equal(mcdcKind, version.mcdcKind)) {
          doc.mcdcKind = mcdcKind;
        }
        if (testIdentifierRule && !equal(testIdentifierRule, version.testIdentifierRule)) {
          doc.testIdentifierRule = testIdentifierRule;
        }
        const bits = tests.testKind.divideZero | tests.testKind.nullPointer | tests.testKind.outOfBounds | tests.testKind.timeout;
        const remainedBits = version.coverages & bits; // 错误推测位原始值
        let currentCoverages = version.coverages;
        if (coverages) {
          doc.coverages = 0;
          if (utility.isArrayType(coverages)) {
            coverages.forEach((coverage) => {
              coverage = Number.parseInt(coverage.toString(), 10);
              doc.coverages |= coverage; // eslint-disable-line no-bitwise
            });
          } else {
            doc.coverages = Number.parseInt(coverages.toString(), 10);
          }
          doc.coverages |= remainedBits; // 保留原始错误推测位
          if (equal(doc.coverages, version.coverages)) {
            delete doc.coverages;
          } else {
            currentCoverages = doc.coverages;
            doc.tasks = {};
          }
        }
        if (enableErrorGuess === true) {
          // 相关位置1
          doc.coverages = currentCoverages | bits;
        } else if (enableErrorGuess === false) {
          // 相关位置0
          doc.coverages = currentCoverages & (~bits);
        }
        if ((caseSensitive || caseSensitive === false)
          && !equal(caseSensitive, version.caseSensitive)) {
          doc.caseSensitive = caseSensitive;
          doc.tasks = {};
        }
        if ((systemType || systemType === 0) && !equal(systemType, version.systemType)) {
          doc.systemType = systemType;
          doc.tasks = {};
        }
        if ((strictOutputCheck || strictOutputCheck === false) && !equal(strictOutputCheck, version.strictOutputCheck)) {
          doc.strictOutputCheck = strictOutputCheck;
        }
        if (status || status === 0) {
          doc.status = Number.parseInt(status.toString(), 10);
          if (equal(doc.status, version.status)) {
            delete doc.status;
          } else if (doc.status === versionStatusKind.frozen) {
            doc.finishAt = moment();
          }
        }
        if (analyzeTemplate
          && (!version.analyzeTemplate
            || !equal(analyzeTemplate, version.analyzeTemplate.toString())
          )
        ) {
          doc.analyzeTemplate = mongoose.Types.ObjectId(analyzeTemplate);
        }
        if (stubDepth && stubDepth > 1 && stubDepth !== version.stubDepth) {
          doc.stubDepth = stubDepth;
        }
        const integrateLibsObj = {
          integrateStdLib: { enabled: integrateStdLib, libs: 'standard' },
          integrateQtLib: { enabled: integrateQtLib, libs: 'qt' },
        };
        const excludeNamespaces = version['exclude namespaces']
          ? [...version['exclude namespaces']] : [];
        let integrateLibsChanged = false;
        Object.keys(integrateLibsObj).forEach((key) => {
          if (integrateLibsObj[key].enabled && !libKind[integrateLibsObj[key].libs].reduce(
            (include, namespace) => {
              const exist = excludeNamespaces.indexOf(namespace) >= 0;
              if (!exist) {
                include = false;
                excludeNamespaces.push(namespace);
              }
              return include;
            }, true
          )) {
            integrateLibsChanged = true;
          }
          if (integrateLibsObj[key].enabled === false && libKind[integrateLibsObj[key].libs].reduce(
            (include, namespace) => {
              const index = excludeNamespaces.indexOf(namespace);
              if (index >= 0) {
                include = true;
                excludeNamespaces.splice(index, 1);
              }
              return include;
            }, false
          )) {
            integrateLibsChanged = true;
          }
        });
        if (integrateLibsChanged) {
          doc['exclude namespaces'] = excludeNamespaces;
        }
        if (eventReminder) {
          Object.keys(eventReminder)
            .forEach((key) => {
              if (!equal(eventReminder[key], version.eventReminder[key])) {
                doc[`eventReminder.${key}`] = eventReminder[key];
              }
            });
        }
        if (commandScriptBeforeParse !== undefined && !equal(commandScriptBeforeParse, version.commandScriptBeforeParse)) {
          doc.commandScriptBeforeParse = commandScriptBeforeParse;
        }
        if (commandScriptBeforeExecute !== undefined && !equal(commandScriptBeforeExecute, version.commandScriptBeforeExecute)) {
          doc.commandScriptBeforeExecute = commandScriptBeforeExecute;
        }
        if (commandScriptBeforeExport !== undefined && !equal(commandScriptBeforeExport, version.commandScriptBeforeExport)) {
          doc.commandScriptBeforeExport = commandScriptBeforeExport;
        }
        if (onlyReplaceStubFunction !== undefined && !equal(onlyReplaceStubFunction, version.onlyReplaceStubFunction)) {
          doc.onlyReplaceStubFunction = onlyReplaceStubFunction;
        }
        if (includedHeaders !== undefined && !equal(includedHeaders, version.includedHeaders)) {
          doc.includedHeaders = includedHeaders;
        }
        if (Object.keys(doc).length) {
          return ProjectVersion.updateOne({ _id: versionId }, { $set: doc });
        }
        // reject without error, 未做任何修改，无需进行后续操作
        return Promise.reject(randId);
      })
      .then(() => getVersion(versionId))
      .then((resVersion) => {
        const keysToBeChanged = Object.keys(doc).filter((key) => {
          if (key.startsWith('eventReminder.')) {
            return false;
          }
          if (key === 'analyzeTemplate') {
            return false;
          }
          return true;
        });
        if (!keysToBeChanged.length) {
          return 'do nothing';
        }
        version = resVersion;
        if (version.masterVersionId) {
          return updateSmartRocketFile(version.masterVersionId);
        }
        return updateSmartRocketFile(version._id);
      })
      .then(() => {
        if (doc.versionName) {
          return tasks.updateProjectVersionName(versionId, doc.versionName);
        }
        return Promise.resolve();
      })
      .then(() => {
        if (doc.stubDepth) {
          return invokeRelations.updateDefaultStubDepth(versionId, doc.stubDepth, originalStubDepth);
        }
        return Promise.resolve();
      })
      .then(() => {
        if (doc.analyzeTemplate) {
          return defects.clearDefects(versionId);
        }
        return Promise.resolve();
      })
      .then(() => updateRegressionStatusPrompt(versionId, 'update'))
      .then((res) => {
        delete doc.tasks;
        delete doc.key;
        doc.versionId = versionId;
        doc.projectId = version.projectId;
        doc.creatorId = version.creatorId;
        publisher.publishProjectVersionMessage('update', doc);
        resolve(res);
      })
      .catch((err) => {
        if (err === randId) {
          return resolve({ update: 1 });
        }
        return reject(err);
      });
  });
}

/**
 * 更新版本环境是否改变的标记
 * @param {String} environmentId 环境id/版本id
 * @param {Boolean} environmentChanged 是否改变
 * @return {Promise}
 */
async function updateEnvironmentChanged(environmentId, environmentChanged) {
  environmentId = mongoose.Types.ObjectId(environmentId);
  // 更新使用该编译环境的版本
  await ProjectVersion.updateMany({ environmentId: { $elemMatch: { $eq: environmentId } } },
    {
      $set: {
        environmentChanged,
      },
    });
}

/**
 * 删除项目版本
 * @param {String} versionId 项目版本id
 * @param {ProjectVersion} version 项目版本
 * @param {Project} project 项目
 * @param {User} creator 创建者
 * @return {Promise}
 */
function removeVersion(versionId, version = undefined, project = undefined, creator = undefined) {
  return new Promise((resolve, reject) => {
    let projectId;
    updateVersion(versionId, { status: versionStatusKind.deleting }, version, project, creator)
      .then(() => common.complementByProjectVersionId(versionId, version, project, creator))
      .then((res) => {
        version = res.version;
        project = res.project;
        creator = res.creator;
        projectId = project._id;
        return eventHub.emit(eventKind.EVENT_VERSION_REMOVE, versionId);
      })
      .then(async () => {
        if (gitClient.isNewEnviroment(version)) {
          return fs.remove(gitClient.getLocalRepoPath(project, version));
        }
        if (!version.masterVersionId) {
          return gitClient.removeBranch(creator, project, version);
        }
        return Promise.resolve();
      })
      .then(() => ProjectVersion.deleteOne({ _id: versionId }))
      .then(() => {
        if (project.lastVersionId.toString() === versionId.toString()) {
          return getLastCreatedVersion({ projectId });
        }
        return Promise.resolve({ _id: project.lastVersionId });
      })
      .then((lastVersion) => {
        if (!lastVersion
          || (lastVersion.projectId && lastVersion.projectId.toString() !== projectId.toString())) {
          // TODO: 删除lastVersionId字段后，原项目无法被find，原因未知
          return Promise.resolve();
        }
        const lastVersionId = lastVersion._id; // eslint-disable-line no-underscore-dangle
        if (project.lastVersionId.toString() === lastVersionId.toString()) {
          return Promise.resolve();
        }
        const data = {
          creatorId: version.creatorId,
          projectId: version.projectId,
          lastVersion: getSummaryJson(lastVersion),
        };
        publisher.publishProjectMessage('update', data);
        return Project.updateOne({ _id: projectId }, { lastVersionId });
      })
      .then(() => {
        if (!version.masterVersionId) {
          return updateRegressionStatusPrompt(versionId, 'remove');
        }
        return Promise.resolve();
      })
      .then(() => {
        const { creatorId, projectId } = version;
        publisher.publishProjectVersionMessage('delete', { creatorId, projectId, versionId });
        resolve();
      })
      .catch(reject);
  });
}

/**
 * 更新访问时间
 * @param {String} versionId 项目版本id
 * @param {ProjectVersion} version 项目版本
 * @param {Project} project 项目
 * @param {User} creator 创建者
 * @return {Promise}
 */
function updateVisitTime(versionId, version = undefined, project = undefined, creator = undefined) {
  return new Promise((resolve, reject) => {
    versionId = mongoose.Types.ObjectId(versionId);
    ProjectVersion.updateOne({ _id: versionId }, { visitAt: moment() })
      .then(() => common.complementByProjectVersionId(versionId, version, project, creator))
      .then(({ version, project }) => {
        if (project.lastVersionId.toString() === versionId.toString()) {
          return Promise.resolve();
        }
        if (!version.masterVersionId) {
          return Project.updateOne({ _id: version.projectId }, { lastVersionId: versionId });
        }
        return Promise.resolve();
      })
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 获取默认编译器
 * @param {String} creatorId 创建者Id
 * @param {String} projectId 项目Id
 * @return {Promise}
 */
function getDefaultProjectType(creatorId, projectId = null) {
  return new Promise((resolve, reject) => {
    Promise.resolve()
      .then(() => getLastCreatedVersion({ creatorId, projectId }))
      .then((version) => {
        if (version && version['project type']) {
          resolve(version['project type']);
        } else {
          resolve();
        }
      })
      .catch(reject);
  });
}

function getLastSelectedTemplate(creatorId) {
  return ProjectVersion.findOne(
    {
      creatorId: mongoose.Types.ObjectId(creatorId),
      analyzeTemplate: { $exists: 1 },
      versionType: versionType.staticAnalyze,
    },
    ['analyzeTemplate']
  ).sort({ _id: -1 })
    .then((version) => {
      if (version) {
        return version.analyzeTemplate;
      }
      return null;
    });
}

async function fileCountInfo(versionId, version = null) {
  const projectVersion = version
    || await ProjectVersion.findById(mongoose.Types.ObjectId(versionId));
  if (!projectVersion) {
    throw new Error(`project version ${versionId} not found`);
  }
  const sourceExts = Object.values(projectVersion.languages)
    .reduce((arr, item) => arr.concat(item.suffixes), []);
  const headerExts = projectVersion.headers.suffixes;
  // 文件数量/代码行数
  return File.aggregate([
    {
      $match: {
        versionId: mongoose.Types.ObjectId(versionId),
        extname: { $in: [...sourceExts, ...headerExts] },
      },
    },
    {
      $group: {
        _id: '$extname',
        fileCount: { $sum: 1 },
        numberOfLines: { $sum: '$numberOfLines' },
        commentLines: { $sum: '$commentLines' },
      },
    },
  ]).then((ret) => ret.reduce((obj, item) => {
    const {
      _id: extname, fileCount, numberOfLines, commentLines,
    } = item;
    obj.commentLines += commentLines;
    obj.numberOfLines += numberOfLines;
    if (headerExts.includes(extname)) {
      obj.headerFileCount += fileCount;
    } else if (sourceExts.includes(extname)) {
      obj.sourceFileCount += fileCount;
    }
    return obj;
  }, {
    sourceFileCount: 0, headerFileCount: 0, numberOfLines: 0, commentLines: 0,
  }));
}

/**
 * 项目版本总览
 * @param {String} versionId 版本id
 * @return {Promise}
 */
async function getOverview(versionId) {
  const projectVersion = await ProjectVersion.findById(mongoose.Types.ObjectId(versionId));
  if (!projectVersion) {
    throw new Error(`project version ${versionId} not found`);
  }
  const sourceExts = Object.values(projectVersion.languages)
    .reduce((arr, item) => arr.concat(item.suffixes), []);
  const headerExts = projectVersion.headers.suffixes;
  const [overview, testCount, functionCount, project, codeDefectData] = await Promise.all([
    // 文件数量/代码行数
    File.aggregate([
      {
        $match: {
          versionId: mongoose.Types.ObjectId(versionId),
          extname: { $in: [...sourceExts, ...headerExts] },
        },
      },
      {
        $lookup: {
          from: 'functions',
          let: { fileId: '$_id' },
          pipeline: [
            {
              $match: {
                $expr: {
                  $and: [
                    { $eq: ['$fileId', '$$fileId'] },
                    { $eq: ['$collectCoverage', true] },
                  ],
                },
              },
            },
            { $project: { _id: 0 } },
          ],
          as: 'funcs',
        },
      },
      {
        $set: {
          collectCoverageFuncsCount: { $size: '$funcs' },
        },
      },
      {
        $match: {
          $or: [
            { collectCoverageFuncsCount: { $gt: 0 } },
            { collectCoverage: true },
            { collectCoverage: { $exists: 0 } },
          ],
        },
      },
      {
        $group: {
          _id: '$extname',
          fileCount: { $sum: 1 },
          numberOfLines: { $sum: '$numberOfLines' },
          commentLines: { $sum: '$commentLines' },
          emptyLines: { $sum: '$emptyLines' },
        },
      },
    ]).then((ret) => ret.reduce((obj, item) => {
      const {
        _id: extname, fileCount, numberOfLines, commentLines, emptyLines,
      } = item;
      obj.numberOfLines += numberOfLines;
      obj.commentLines += commentLines;
      obj.emptyLines += emptyLines;
      if (headerExts.includes(extname)) {
        obj.headerFileCount += fileCount;
        obj.headerNumberOfLines += numberOfLines;
        obj.headerCommentLines += commentLines;
        obj.headerEmptyLines += emptyLines;
      } else if (sourceExts.includes(extname)) {
        obj.sourceFileCount += fileCount;
        obj.sourceNumberOfLines += numberOfLines;
        obj.sourceCommentLines += commentLines;
        obj.sourceEmptyLines += emptyLines;
      }
      return obj;
    }, {
      sourceFileCount: 0,
      headerFileCount: 0,
      numberOfLines: 0,
      commentLines: 0,
      emptyLines: 0,
      headerNumberOfLines: 0,
      headerCommentLines: 0,
      headerEmptyLines: 0,
      sourceNumberOfLines: 0,
      sourceCommentLines: 0,
      sourceEmptyLines: 0,
    })),
    // 测试用例数量
    Test.aggregate([
      {
        $match: { versionId: mongoose.Types.ObjectId(versionId) },
      },
      {
        $lookup: {
          from: 'functions',
          localField: 'functionId',
          foreignField: '_id',
          as: 'function',
        },
      },
      {
        $unwind: '$function',
      },
      {
        $match: {
          'function.collectCoverage': true,
        },
      },
      { $group: { _id: null, count: { $sum: 1 } } },
    ])
      .then((res) => {
        if (res[0]) {
          return res[0].count;
        }
        return 0;
      }),
    // 函数数量
    Function.countDocuments({
      versionId: mongoose.Types.ObjectId(versionId),
      kind: { $in: fileElements.getCountableKinds() },
      collectCoverage: true,
      $or: [
        { insideProject: { $eq: true } },
        { insideProject: { $exists: 0 } },
      ],
    }),
    // project info
    Project.findById(projectVersion.projectId),
    // code defects
    codeDefects.getCodeDefects(null, { versionId }),
  ]);

  const changedFunctionInRegeression = {};
  if (projectVersion.oldVersionId) {
    const changedFunctionCount = await Function.countDocuments({
      versionId: mongoose.Types.ObjectId(versionId),
      kind: { $in: fileElements.getCountableKinds() },
      collectCoverage: true,
      $or: [
        { insideProject: { $eq: true } },
        { insideProject: { $exists: 0 } },
      ],
      changed: { $exists: true },
    });
    changedFunctionInRegeression.changedFunctionCount = changedFunctionCount;
    overview.oldVersionName = (await ProjectVersion.findById(mongoose.Types.ObjectId(projectVersion.oldVersionId))).versionName;
  }

  const { rootDirectoryId } = projectVersion;
  const rootDir = await File.findById(rootDirectoryId);
  const {
    statementCovered,
    statementTotal,
    branchCovered,
    branchTotal,
    conditionCovered,
    conditionTotal,
    decisionCovered,
    decisionTotal,
    mcdcCovered,
    mcdcTotal,
    invokedFuncTotal,
    invokedFuncCovered,
    invokeTotal,
    invokeCovered,
  } = common.getCountOfFile(rootDir);

  return {
    ...overview,
    testCount,
    functionCount,
    codeDefectCount: codeDefectData.length,
    statementCovered,
    statementTotal,
    branchCovered,
    branchTotal,
    conditionCovered,
    conditionTotal,
    decisionCovered,
    decisionTotal,
    mcdcCovered,
    mcdcTotal,
    invokedFuncTotal,
    invokedFuncCovered,
    invokeTotal,
    invokeCovered,
    projectName: project ? project.projectName : '',
    projectId: projectVersion.projectId,
    _id: projectVersion._id,
    versionName: projectVersion.versionName,
    ...changedFunctionInRegeression,
  };
}

/**
 * 测试用例执行结果总览
 * @param {String} versionId 版本id
 * @return {Promise}
 */
async function testResultOverview(versionId) {
  const ret = await Test.aggregate([
    {
      $match: {
        versionId: mongoose.Types.ObjectId(versionId),
      },
    },
    {
      $lookup: {
        from: 'functions',
        localField: 'functionId',
        foreignField: '_id',
        as: 'function',
      },
    },
    {
      $unwind: '$function',
    },
    {
      $match: {
        'function.collectCoverage': true,
      },
    },
    {
      // 是否手动
      $addFields: {
        manually: { $eq: ['$kind', tests.testKind.modified] },
      },
    },
    {
      $group: {
        _id: {
          manually: '$manually',
          resultKind: '$resultKind',
        },
        count: { $sum: 1 },
      },
    },
  ]);

  const resultSet = Object.values(tests.resultKind);
  const summary = {
    manually: resultSet.reduce((obj, r) => {
      obj[r] = 0;
      return obj;
    }, {}),
    automatically: resultSet.reduce((obj, r) => {
      obj[r] = 0;
      return obj;
    }, {}),
  };
  return ret.reduce((_summary, item) => {
    const { manually, resultKind } = item._id;
    const { count } = item;
    const obj = manually ? _summary.manually : _summary.automatically;
    obj[resultKind] = count;
    return _summary;
  }, summary);
}

/**
 * 函数覆盖率分布统计
 * @param {String} versionId 版本id
 * @return {Promise}
 */
async function functionCoverageDistribution(versionId) {
  const interval = 25;
  const projectVersion = await ProjectVersion.findById(mongoose.Types.ObjectId(versionId));
  const functions = await Function.find({
    versionId: mongoose.Types.ObjectId(versionId),
    collectCoverage: true,
    kind: { $in: fileElements.getCountableKinds() },
  });
  const summary = {};
  const coverageNames = [];
  Object.keys(tests.coverageKind).forEach((coverageName) => {
    const coverage = tests.coverageKind[coverageName];
    if (coverage === tests.coverageKind.all) {
      return;
    }
    if ((projectVersion.coverages & coverage) > 0) { // eslint-disable-line no-bitwise
      coverageNames.push(coverageName);
    }
  });

  coverageNames.forEach((coverageName) => {
    const segment = Math.floor(100 / interval + 1);
    summary[coverageName] = { distribution: new Array(segment).fill(0) };
  });
  functions.forEach((func) => {
    coverageNames.forEach((coverageName) => {
      const covered = func[`${coverageName}Covered`];
      const total = func[`${coverageName}Total`];
      if (total) {
        const index = Math.floor((covered * 100) / total / interval);
        summary[coverageName].distribution[index] += 1;
      }
    });
  });

  return summary;
}

async function getOverviewAll(versionId) {
  const vid = mongoose.Types.ObjectId(versionId);
  const projectVersions = await ProjectVersion.aggregate([
    {
      $match: { $or: [{ _id: vid }, { masterVersionId: vid }] },
    },
    {
      $lookup: {
        from: 'projects', localField: 'projectId', foreignField: '_id', as: 'project',
      },
    },
    {
      $unwind: '$project',
    },
    {
      $sort: {
        masterVersionId: 1,
      },
    },
  ]);
  if (!projectVersions.length) {
    throw new PhoenixError('not found', `project version ${versionId} not exist`);
  }
  const [masterVersion] = projectVersions;

  const sourceExts = Object.values(masterVersion.languages)
    .reduce((arr, item) => arr.concat(item.suffixes), []);
  const headerExts = masterVersion.headers.suffixes;
  const fileCount = await File.aggregate([
    {
      $match: {
        versionId: masterVersion._id,
        extname: { $in: [...sourceExts, ...headerExts] },
      },
    },
    {
      $group: {
        _id: '$extname',
        fileCount: { $sum: 1 },
        numberOfLines: { $sum: '$numberOfLines' },
      },
    },
  ]).then((ret) => ret.reduce((obj, item) => {
    const { _id: extname, fileCount, numberOfLines } = item;
    obj.numberOfLines += numberOfLines;
    if (headerExts.includes(extname)) {
      obj.headerFileCount += fileCount;
    } else if (sourceExts.includes(extname)) {
      obj.sourceFileCount += fileCount;
    }
    return obj;
  }, { sourceFileCount: 0, headerFileCount: 0, numberOfLines: 0 }));

  const result = {
    versionId,
    versionName: masterVersion.versionName,
    projectId: masterVersion.project._id,
    projectName: masterVersion.project.projectName,
    sourceFileCount: fileCount.sourceFileCount,
    headerFileCount: fileCount.headerFileCount,
    integrationTest: {
      testCount: {
        manually: 0,
        automatically: 0,
      },
      invokedFuncCoverage: 0,
      invokeCoverage: 0,
    },
    unitTest: {
      testCount: {
        manually: 0,
        automatically: 0,
      },
      statementCoverage: 0,
      branchCoverage: 0,
      conditionCoverage: 0,
      decisionCoverage: 0,
      mcdcCoverage: 0,
    },
    defect: {
      count: {
        mandatory: 0,
        required: 0,
        advisory: 0,
        designTaboo: 0,
      },
      kloc: {
        mandatory: 0,
        required: 0,
        advisory: 0,
        designTaboo: 0,
      },
    },
  };
  await Promise.all(
    projectVersions.map(async (projectVersion) => {
      let vType = '';
      if (projectVersion.versionType) {
        vType = projectVersion.versionType;
      }
      const { rootDirectoryId, coverages } = projectVersion;
      const defaultValue = {
        statementCoverage: coverages & tests.coverageKind.statement ? 0 : undefined,
        branchCoverage: coverages & tests.coverageKind.branch ? 0 : undefined,
        conditionCoverage: coverages & tests.coverageKind.condition ? 0 : undefined,
        decisionCoverage: coverages & tests.coverageKind.decision ? 0 : undefined,
        mcdcCoverage: coverages & tests.coverageKind.mcdc ? 0 : undefined,
        invokedFuncCoverage: coverages & tests.coverageKind.invokedFunc ? 0 : undefined,
        invokeCoverage: coverages & tests.coverageKind.invoke ? 0 : undefined,
        testCount: {
          manually: 0,
          automatically: 0,
        },
      };
      let key = 'unitTest';
      switch (vType) {
        case versionType.unitTest:
          result[key] = defaultValue;
          break;
        case versionType.integrationTest:
          key = 'integrationTest';
          result[key] = defaultValue;
          break;
        default:
          break;
      }
      const rootDir = await File.findById(rootDirectoryId);
      if (!rootDir) {
        return;
      }
      if (vType !== versionType.staticAnalyze) {
        const {
          statementCovered,
          statementTotal,
          branchCovered,
          branchTotal,
          conditionCovered,
          conditionTotal,
          decisionCovered,
          decisionTotal,
          mcdcCovered,
          mcdcTotal,
          invokeCovered,
          invokeTotal,
          invokedFuncCovered,
          invokedFuncTotal,
        } = common.getCountOfFile(rootDir);
        const functionCount = await Function.countDocuments({
          versionId: projectVersion._id,
          kind: { $in: fileElements.getCountableKinds() },
          $or: [
            { insideProject: { $eq: true } },
            { insideProject: { $exists: 0 } },
          ],
        });
        const testCount = (await Test.aggregate([
          {
            $match: {
              versionId: projectVersion._id,
            },
          },
          {
            // 是否手动
            $addFields: {
              manually: { $eq: ['$kind', tests.testKind.modified] },
            },
          },
          {
            $group: {
              _id: '$manually',
              count: { $sum: 1 },
            },
          },
        ])).reduce((obj, item) => {
          const key = item._id ? 'manually' : 'automatically';
          obj[key] += item.count;
          return obj;
        }, {
          manually: 0,
          automatically: 0,
        });
        const moduleData = {
          functionCount,
          testCount,
          statementCoverage: coverages & tests.coverageKind.statement ? (statementCovered / statementTotal) || 0 : undefined,
          branchCoverage: coverages & tests.coverageKind.branch ? (branchCovered / branchTotal) || 0 : undefined,
          conditionCoverage: coverages & tests.coverageKind.condition ? (conditionCovered / conditionTotal) || 0 : undefined,
          decisionCoverage: coverages & tests.coverageKind.decision ? (decisionCovered / decisionTotal) || 0 : undefined,
          mcdcCoverage: coverages & tests.coverageKind.mcdc ? (mcdcCovered / mcdcTotal) || 0 : undefined,
        };
        if (vType === versionType.integrationTest) {
          moduleData.invokedFuncCoverage = coverages & tests.coverageKind.invokedFunc ? (invokedFuncCovered / invokedFuncTotal) || 0 : undefined;
          moduleData.invokeCoverage = coverages & tests.coverageKind.invoke ? (invokeCovered / invokeTotal) || 0 : undefined;
        }
        result[key] = moduleData;
      }
      if (vType === versionType.staticAnalyze || vType === '') {
        const { numberOfLines } = fileCount;
        const { defectsCount } = rootDir;
        const {
          mandatory, required, advisory, designTaboo,
        } = defectsCount || {};
        const count = {
          mandatory: mandatory || 0,
          required: required || 0,
          advisory: advisory || 0,
          designTaboo: designTaboo || 0,
        };
        const kloc = {
          mandatory: Math.ceil((count.mandatory / numberOfLines) * 1000),
          required: Math.ceil((count.required / numberOfLines) * 1000),
          advisory: Math.ceil((count.advisory / numberOfLines) * 1000),
          designTaboo: Math.ceil((count.designTaboo / numberOfLines) * 1000),
        };
        result.defect = { count, kloc };
      }
    })
  );
  return result;
}

function getProjectVersionStatistics(versionId, version = null) {
  const statistics = {
    function: {
      total: 0,
    },
    file: {
      total: 0,
      headers: {
        total: 0,
      },
      languages: {
        c: {
          total: 0,
        },
        'c++': {
          total: 0,
        },
      },
      notCompiledCount: 0,
      compileFailedCount: 0,
      compiledCount: 0,
      waiting4compiling: 0,
      coverage: 0,
      codeDefects: 0,
      testResult: {
        unknown: 0,
        succeed: 0,
        failed: 0,
        noResult: 0,
        runtimeError: 0,
        exception: 0,
        nonStrictSucceed: 0,
      },
    },
  };
  let extensions = [];
  let headersSet;
  let cSet;
  let cPlusSet;
  const testedFuncsCount = {};
  return Promise.resolve()
    .then(() => {
      if (version) {
        return Promise.resolve(version);
      }
      return getVersion(versionId);
    })
    .then((version) => {
      headersSet = new Set(version.headers.suffixes);
      let cSuffixes = [];
      let cPlusSuffixes = [];
      Object.keys(version.languages).forEach((key) => {
        if (key === 'c') {
          cSuffixes = cSuffixes.concat(version.languages[key].suffixes);
        } else if (key === 'c++') {
          cPlusSuffixes = cPlusSuffixes.concat(version.languages[key].suffixes);
        }
      });
      extensions = extensions.concat(cSuffixes);
      extensions = extensions.concat(cPlusSuffixes);
      extensions = extensions.concat(version.headers.suffixes);
      extensions = utility.arrayDeduplicate(extensions);
      cSet = new Set(cSuffixes);
      cPlusSet = new Set(cPlusSuffixes);
      const addFields = {};
      const match = [];
      if ((version.coverages & tests.coverageKind.statement) > 0) {
        addFields.st = { $sum: extensions.map((ext) => `$statementTotal.${common.renameMongoKey(ext)}`) };
        addFields.sc = { $sum: extensions.map((ext) => `$statementCovered.${common.renameMongoKey(ext)}`) };
        match.push({ $expr: { $lt: ['$sc', '$st'] } });
      }
      if ((version.coverages & tests.coverageKind.condition) > 0) {
        addFields.ct = { $sum: extensions.map((ext) => `$conditionTotal.${common.renameMongoKey(ext)}`) };
        addFields.cc = { $sum: extensions.map((ext) => `$conditionCovered.${common.renameMongoKey(ext)}`) };
        match.push({ $expr: { $lt: ['$cc', '$ct'] } });
      }
      if ((version.coverages & tests.coverageKind.branch) > 0) {
        addFields.bt = { $sum: extensions.map((ext) => `$branchTotal.${common.renameMongoKey(ext)}`) };
        addFields.bc = { $sum: extensions.map((ext) => `$branchCovered.${common.renameMongoKey(ext)}`) };
        match.push({ $expr: { $lt: ['$bc', '$bt'] } });
      }
      if ((version.coverages & tests.coverageKind.mcdc) > 0) {
        addFields.mt = { $sum: extensions.map((ext) => `$mcdcTotal.${common.renameMongoKey(ext)}`) };
        addFields.mc = { $sum: extensions.map((ext) => `$mcdcCovered.${common.renameMongoKey(ext)}`) };
        match.push({ $expr: { $lt: ['$mc', '$mt'] } });
      }
      if ((version.coverages & tests.coverageKind.decision) > 0) {
        addFields.dt = { $sum: extensions.map((ext) => `$decisionTotal.${common.renameMongoKey(ext)}`) };
        addFields.dc = { $sum: extensions.map((ext) => `$decisionCovered.${common.renameMongoKey(ext)}`) };
        match.push({ $expr: { $lt: ['$dc', '$dt'] } });
      }
      if ((version.coverages & tests.coverageKind.invoke) > 0) {
        addFields.it = { $sum: extensions.map((ext) => `$invokeTotal.${common.renameMongoKey(ext)}`) };
        addFields.ic = { $sum: extensions.map((ext) => `$invokeCovered.${common.renameMongoKey(ext)}`) };
        match.push({ $expr: { $lt: ['$ic', '$it'] } });
      }
      if ((version.coverages & tests.coverageKind.invokedFunc) > 0) {
        addFields.ift = { $sum: extensions.map((ext) => `$invokedFuncTotal.${common.renameMongoKey(ext)}`) };
        addFields.ifc = { $sum: extensions.map((ext) => `$invokedFuncCovered.${common.renameMongoKey(ext)}`) };
        match.push({ $expr: { $lt: ['$ifc', '$ift'] } });
      }
      return Promise.all([
        files.getGroupCount(versionId, 'extname'),
        files.getGroupCount(versionId, 'status', { extensions }),
        File.aggregate(
          [
            { $match: { versionId: mongoose.Types.ObjectId(versionId), kind: files.kind.file, extname: { $in: extensions } } },
            {
              $addFields: addFields,
            },
            {
              $match:
              {
                $or: match,
              },
            },
            {
              $count: 'totalCount',
            },
          ]
        ),
      ]);
    })
    .then(([extGroup, statusGroup, coverageGroup]) => {
      statistics.file.coverage = coverageGroup.length === 0 ? 0 : coverageGroup[0].totalCount;
      extGroup.forEach(({ _id, count }) => {
        if (headersSet.has(_id)) {
          statistics.file.total += count;
          statistics.file.headers.total += count;
        } else if (cSet.has(_id)) {
          statistics.file.total += count;
          statistics.file.languages.c.total += count;
        } else if (cPlusSet.has(_id)) {
          statistics.file.total += count;
          statistics.file.languages['c++'].total += count;
        }
      });
      statusGroup.forEach(({ _id, count }) => {
        if (_id === files.statusKind.compiled) {
          statistics.file.compiledCount += count;
        } else if (_id === files.statusKind.compileFailed) {
          statistics.file.compileFailedCount += count;
        } else if (_id === files.statusKind.notCompiled) {
          statistics.file.notCompiledCount += count;
        } else if (_id === files.statusKind.waiting4compiling) {
          statistics.file.waiting4compiling += 1;
        }
      });
      return Promise.all([
        tests.getTestResultsGroup(['fileId', 'functionId', 'resultKind'], { versionId }),
        codeDefects.getCodeDefectsGroup('fileId', { versionId }),
      ]);
    })
    .then(([resultKinds, codeDefects]) => {
      const fileResultKinds = {};
      Object.keys(resultKinds).forEach((key) => {
        // eslint-disable-next-line no-unused-vars
        const [fileId, functionId, resultKind] = key.split('|');
        if (Number(resultKind) !== tests.resultKind.unknown) {
          testedFuncsCount[functionId] = 1;
        }
        let resultKey = '';
        switch (Number(resultKind)) {
          case tests.resultKind.unknown:
            resultKey = 'unknown';
            break;
          case tests.resultKind.succeed:
            resultKey = 'succeed';
            break;
          case tests.resultKind.failed:
            resultKey = 'failed';
            break;
          case tests.resultKind.noResult:
            resultKey = 'noResult';
            break;
          case tests.resultKind.runtimeError:
          case tests.resultKind.timeout:
            resultKey = 'runtimeError';
            break;
          case tests.resultKind.systemError:
          case tests.resultKind.instrumentError:
          case tests.resultKind.compileError:
            resultKey = 'exception';
            break;
          case tests.resultKind.nonStrictSucceed:
            resultKey = 'nonStrictSucceed';
            break;
          default:
            break;
        }
        if (!fileResultKinds[`${fileId}|${resultKey}`]) {
          fileResultKinds[`${fileId}|${resultKey}`] = 1;
          statistics.file.testResult[resultKey] += 1;
        }
      });
      statistics.file.codeDefects = Object.keys(codeDefects).length;
      return functions.getCountByExtname(versionId, extensions);
    })
    .then(({ total, modified, added }) => {
      statistics.function.total = total;
      statistics.function.testedTotal = Object.keys(testedFuncsCount).length;
      statistics.functionChange = {};
      statistics.functionChange.addedCount = added;
      statistics.functionChange.modifiedCount = modified;
      return statistics;
    });
}

async function diffProjectVersions($me, currentVersionId, targetVersionId) {
  const retDiffData = {
    addFiles: [],
    modifyFiles: [],
    deleteFiles: [],
    insertion: 0,
    deletion: 0,
  };
  const targetVersion = await ProjectVersion.findById(mongoose.Types.ObjectId(targetVersionId));
  if (!targetVersion) {
    throw new PhoenixError('not found', `project target version ${targetVersionId} not exist`);
  }
  const currentVersion = await ProjectVersion.findById(mongoose.Types.ObjectId(currentVersionId));
  if (!currentVersion) {
    throw new PhoenixError('not found', `project current version ${currentVersionId} not exist`);
  }
  const project = await Project.findById(currentVersion.projectId);
  const diffData = await gitClient.diffSummaryFormat($me, project, targetVersion.currentCommitId, currentVersion.currentCommitId);
  const suffixes = await getVersionSuffixes(currentVersionId);
  diffData.files.forEach((file) => {
    let inFileFilter = false;
    global.config.fileFilters.forEach((fileFilter) => {
      if (file.file.includes(fileFilter)) {
        inFileFilter = true;
      }
    });
    if (inFileFilter) {
      return;
    }
    const fileSuffix = path.extname(file.file);
    if (!suffixes.includes(fileSuffix)) {
      return;
    }
    const typeAlias = {
      A: 'addFiles',
      M: 'modifyFiles',
      R: 'modifyFiles',
      D: 'deleteFiles',
    };
    switch (file.type) {
      case gitClient.diffType.add:
      case gitClient.diffType.modify:
      case gitClient.diffType.rename:
      case gitClient.diffType.delete:
        retDiffData[typeAlias[file.type]].push(file.file);
        retDiffData.insertion += file.insertion;
        retDiffData.deletion += file.deletion;
        break;
      default:
        break;
    }
  });

  return retDiffData;
}

async function namesSet(projectName, userId, {
  lastProjectOnly = false, // 只显示用户当前项目
  lastVersionOnly = false, // 只显示项目的当前版本
} = {}) {
  const projectExec = Project.aggregate();
  if (userId) {
    projectExec.append({ $match: { creatorId: mongoose.Types.ObjectId(userId) } });
  }
  if (lastProjectOnly) {
    projectExec.append({
      $group: {
        _id: '$creatorId',
        projects: {
          $push: {
            creatorId: '$creatorId',
            _id: '$_id',
            projectName: '$projectName',
            lastVersionId: '$lastVersionId',
            visitAt: '$visitAt',
          },
        },
      },
    },
    {
      $replaceRoot: {
        newRoot: {
          // 只取最后访问项目
          $reduce: {
            input: '$projects',
            initialValue: null,
            in: {
              $cond: {
                if: { $gt: ['$$this.visitAt', '$$value.visitAt'] },
                then: '$$this',
                else: '$$value',
              },
            },
          },
        },
      },
    });
  }
  if (projectName) {
    projectExec.append({ $match: { projectName } });
  } else {
    projectExec.append({ $match: { projectName: { $ne: '' } } });
  }
  if (lastVersionOnly) {
    // 取最新版本
    projectExec.append({
      $lookup: {
        from: 'projectversions', localField: 'lastVersionId', foreignField: '_id', as: 'version',
      },
    }, {
      $unwind: '$version',
    });
  } else {
    // 取所有版本
    projectExec.append({
      $lookup: {
        from: 'projectversions', localField: '_id', foreignField: 'projectId', as: 'version',
      },
    }, {
      $unwind: '$version',
    });
  }
  projectExec.append(
    {
      $group: {
        _id: null,
        names: {
          $addToSet: '$version.versionName',
        },
      },
    }
  );

  const arr = await projectExec.exec();
  return arr.length ? arr[0].names.sort() : [];
}

async function getStaticAnalyzeOverviewList({
  projectName = undefined,
  versionName = undefined,
  userId,
  q,
  page = 1,
  pageSize = 10,
  sortBy = 'versionId',
  order = 'asc',
  lastProjectOnly = false, // 只显示用户当前项目
  lastVersionOnly = false, // 只显示项目的当前版本
} = {}) {
  if (sortBy === 'defectsCount') {
    sortBy = 'defectsCount.total';
  } else if (sortBy === 'userName') {
    sortBy = 'userNameLowerCase';
  }
  const projectExec = Project.aggregate();
  if (userId) {
    projectExec.append({ $match: { creatorId: mongoose.Types.ObjectId(userId) } });
  }
  if (lastProjectOnly) {
    // 取每个用户的最新项目
    projectExec.append({
      $group: {
        _id: '$creatorId',
        projects: {
          $push: {
            creatorId: '$creatorId',
            _id: '$_id',
            projectName: '$projectName',
            lastVersionId: '$lastVersionId',
            visitAt: '$visitAt',
          },
        },
      },
    },
    {
      $replaceRoot: {
        newRoot: {
          // 只取最后访问项目
          $reduce: {
            input: '$projects',
            initialValue: null,
            in: {
              $cond: {
                if: { $gt: ['$$this.visitAt', '$$value.visitAt'] },
                then: '$$this',
                else: '$$value',
              },
            },
          },
        },
      },
    });
  }
  if (projectName) {
    projectExec.append({ $match: { projectName } });
  } else {
    projectExec.append({ $match: { projectName: { $ne: '' } } });
  }
  if (lastVersionOnly) {
    // 取最新版本
    projectExec.append({
      $lookup: {
        from: 'projectversions', localField: 'lastVersionId', foreignField: '_id', as: 'version',
      },
    }, {
      $unwind: '$version',
    });
  } else {
    // 取所有版本
    projectExec.append({
      $lookup: {
        from: 'projectversions', localField: '_id', foreignField: 'projectId', as: 'version',
      },
    }, {
      $unwind: '$version',
    });
  }
  if (versionName) {
    projectExec.append({ $match: { 'version.versionName': versionName } });
  }
  projectExec.append(
    {
      $match: {
        'version.masterVersionId': null, // 只取master version
      },
    },
    {
      $lookup: {
        from: 'projectversions', localField: 'version._id', foreignField: 'masterVersionId', as: 'subVersion',
      },
    },
    {
      $unwind: { path: '$subVersion', preserveNullAndEmptyArrays: true },
    },
    {
      $match: {
        $or: [
          { subVersion: null },
          { 'subVersion.versionType': 'staticAnalyze' }],
      },
    }
  );
  projectExec.append({
    $lookup: {
      from: 'users', localField: 'version.creatorId', foreignField: '_id', as: 'user',
    },
  },
  {
    $unwind: { path: '$user', preserveNullAndEmptyArrays: true },
  });
  if (q) {
    projectExec.append({
      $match: {
        $or: [
          { 'user.username': RegExp(escapeRegexp(q)) },
          { projectName: RegExp(escapeRegexp(q)) }],
      },
    });
  }
  // 查询总数
  const countRet = await Project.aggregate(projectExec.pipeline()).append({ $count: 'total' });
  const [{ total }] = countRet && countRet.length ? countRet : [{ total: 0 }];
  projectExec.append({
    $replaceRoot: {
      newRoot: {
        projectId: '$_id',
        projectName: '$projectName',
        versionId: '$version._id',
        versionName: '$version.versionName',
        subVersion: '$subVersion',
        rootDirectoryId: '$subVersion.rootDirectoryId',
        creatorId: '$version.creatorId',
        user: '$user',
      },
    },
  });
  projectExec.append({
    $lookup: {
      from: 'files', localField: 'rootDirectoryId', foreignField: '_id', as: 'rootDirectory',
    },
  },
  {
    $unwind: { path: '$rootDirectory', preserveNullAndEmptyArrays: true },
  },
  {
    $set: {
      defectsCount: {
        $mergeObjects: [{
          mandatory: 0,
          required: 0,
          advisory: 0,
          designTaboo: 0,
        }, '$rootDirectory.defectsCount'],
      },
      userId: '$user._id',
      userName: '$user.username',
      userNameLowerCase: { $toLower: '$user.username' },
    },
  },
  {
    $set: {
      'defectsCount.total': {
        $add: ['$defectsCount.mandatory', '$defectsCount.required', '$defectsCount.advisory', '$defectsCount.designTaboo'],
      },
    },
  },
  {
    $sort: {
      [sortBy]: order.toLowerCase() === 'asc' ? 1 : -1,
    },
  },
  {
    $skip: (page - 1) * pageSize,
  },
  {
    $limit: pageSize,
  },
  {
    $lookup: {
      from: 'tasks',
      localField: 'subVersion._id',
      foreignField: 'versionId',
      as: 'tasks',
    },
  },
  {
    $set: {
      tasks: {
        $map: {
          input: {
            $filter: {
              input: '$tasks',
              as: 'task',
              cond: { $in: ['$$task.type', [tasks.type.analyze, tasks.type.regressionAnalyze]] },
            },
          },
          as: 'task',
          in: '$$task._id',
        },
      },
    },
  },
  {
    $set: {
      latestTask: { $max: '$tasks' },
      times: { $size: '$tasks' },
    },
  },
  {
    $unset: ['rootDirectory', 'rootDirectoryId', 'user', 'defectsCount.total', 'tasks', 'creatorId', 'userNameLowerCase', 'subVersion'],
  });
  const versions = await projectExec.exec();

  return {
    total,
    versions: versions.map((version) => {
      const newObj = {
        ...version,
        executedAt: version.latestTask ? version.latestTask.getTimestamp().getTime() : undefined,
      };
      delete newObj.latestTask;
      return newObj;
    }),
  };
}
/**
 * 创建项目回归版本
 * @param {*} projectId 项目id
 * @param {*} doc 版本信息
 * @param {*} project 项目
 * @param {*} creator 创建者
 * @returns
 */
function addRegressionVersion(projectId, doc, project = undefined, creator = undefined) {
  return new Promise((resolve, reject) => {
    const originalversionId = mongoose.Types.ObjectId(doc.oldVersionId);
    projectId = mongoose.Types.ObjectId(projectId);
    let version;
    let versionId;
    let creatorId;
    let rootDirectory;
    doc.projectId = projectId;
    doc.key = getVersionKey(projectId, doc.versionName);
    doc.visitAt = moment();
    doc.version = global.config.configurationVersion;
    const updateInfo = { status: versionStatusKind.normal };
    ProjectVersion.findOne({ _id: originalversionId })
      .then((originalVersion) => {
        doc.oldVersionId = originalversionId;
        doc.coverages = originalVersion.coverages;
        doc.mcdcKind = originalVersion.mcdcKind;
        doc.timeout = originalVersion.timeout;
        doc.environmentChange = originalVersion.environmentChange;
        doc.languages = originalVersion.languages;
        doc.caseSensitive = originalVersion.caseSensitive;
        doc.headers = originalVersion.headers;
        doc.exits = originalVersion.exits;
        doc.strictOutputCheck = originalVersion.strictOutputCheck;
        doc.integrateStdLib = originalVersion.integrateStdLib;
        doc.integrateQtLib = originalVersion.integrateQtLib;
        doc.language = originalVersion.language;
        doc.environment = originalVersion.environment;
        doc.systems = originalVersion.systems;
        doc.systemType = originalVersion.systemType;
        doc.analyzeTemplate = originalVersion.analyzeTemplate;
        doc.timeoutTestGen = originalVersion.timeoutTestGen;
        doc.timeoutTestRun = originalVersion.timeoutTestRun;
        doc.macros = originalVersion.macros;
        doc.autoNullCheck = originalVersion.autoNullCheck;
        doc['ignore asm'] = originalVersion['ignore asm'];
        doc['project type'] = originalVersion['project type'];
        doc['exclude namespaces'] = originalVersion['exclude namespaces'];
        doc.environmentMacrosShow = originalVersion.environmentMacrosShow;
        doc.environmentArgumentsShow = originalVersion.environmentArgumentsShow;
        doc.includes = originalVersion.includes;
        doc.arguments = originalVersion.arguments;
        doc.environmentId = originalVersion.environmentId;
        doc.commandScriptBeforeParse = originalVersion.commandScriptBeforeParse;
        doc.commandScriptBeforeExecute = originalVersion.commandScriptBeforeExecute;
        doc.commandScriptBeforeExport = originalVersion.commandScriptBeforeExport;
        doc.onlyReplaceStubFunction = originalVersion.onlyReplaceStubFunction;
        doc.includedHeaders = originalVersion.includedHeaders;
      })
      .then(() => common.complementByProjectId(projectId, project, creator))
      .then((res) => {
        project = res.project;
        creator = res.creator;
        creatorId = creator._id;
        doc.creatorId = creatorId;
      })
      .then(() => getLastSelectedTemplate(creatorId))
      .then((lastSelectTemplate) => {
        const { analyzeTemplate } = doc;
        if (analyzeTemplate) {
          doc.analyzeTemplate = mongoose.Types.ObjectId(analyzeTemplate);
        } else if (lastSelectTemplate) {
          doc.analyzeTemplate = lastSelectTemplate;
        }
      })
      .then(() => machine.getCompilationOptions())
      .then((options) => {
        const {
          projectType,
          environment,
        } = doc;
        if (!environment) {
          doc.environment = options['project types'][projectType].environment;
        }
        const version = new ProjectVersion(doc);
        return version.save();
      })
      .then((res) => {
        version = res;
        versionId = version._id; // eslint-disable-line no-underscore-dangle
        const { repository } = version;
        repository.url = gitServer.getRemoteURL(creator, project);
        updateInfo.repository = repository;
        return files.addDirectory(creatorId, projectId, versionId, '.');
      })
      .then((dir) => {
        rootDirectory = dir;
        updateInfo.rootDirectoryId = dir._id; // eslint-disable-line no-underscore-dangle
        return gitClient.createBranch(creator, project, version);
      })
      .then(() => gitClient.getLatestCommitId(creator, project, version))
      .then((commitId) => {
        updateInfo.currentCommitId = commitId;
        return ProjectVersion.updateOne({ _id: versionId }, updateInfo);
      }, (err) => ProjectVersion.deleteOne({ _id: versionId })
        .then(() => { throw err; }))
      .then(() => Project.updateOne({ _id: projectId }, { lastVersionId: versionId }))
      .then(() => getVersion(versionId))
      .then((resVersion) => {
        version = resVersion;
        const kind = files.kind.file;
        const fileName = files.VERSIONFIELNAME;
        const content = JSON.stringify(version);
        return files.addFile2Directory(
          updateInfo.rootDirectoryId,
          { kind, fileName, content },
          rootDirectory,
          version,
          project,
          creator,
        );
      })
      .then(() => {
        const data = getJson(version);
        data.projectId = projectId;
        data.creatorId = project.creatorId;
        publisher.publishProjectVersionMessage('add', data);
        resolve(version);
      })
      .catch(reject);
  });
}

async function getVersionConfig(versionId) {
  await defects.removeDefectsByLicense(versionId);
  const versionConfig = { versionId };
  let version = await getVersion(versionId);
  // 处理老项目的编译环境旧数据结构，删除system
  if (version.system && Object.keys(version.system).length > 0) {
    await updateSmartRocketFile(version._id);
    version = await getVersion(versionId);
    delete version.system;
  }
  const analyzeTemplate = await getLastSelectedTemplate(version.creatorId);
  version = getJson(version);
  Object.keys(version).forEach((key) => {
    if (versionConfigFields.commonFields.includes(key)) {
      versionConfig[key] = version[key];
    }
  });

  versionConfig.rootDirectory = version.rootDirectory;
  versionConfig.unitTest = {
    eventReminder: {
      configConfirmed: false,
      regressionStatusPrompt: regressionStatusPrompt.none,
      firstVisit: 0,
      parsed: false,
    },
    timeoutTestGen: 10,
    timeoutTestRun: 10,
    coverages: tests.coverageKind.statement | tests.coverageKind.branch | tests.coverageKind.mcdc,
    status: versionStatusKind.normal,
    strictOutputCheck: true,
    integrateStdLib: version.integrateStdLib,
    integrateQtLib: version.integrateQtLib,
    collectCoverageKind: version.collectCoverageKind,
    autoNullCheck: false,
    dataDepth: 6,
    mcdcKind: tests.mcdcKind.masking,
  };
  versionConfig.staticAnalyze = {
    eventReminder: {
      configConfirmed: false,
      regressionStatusPrompt: regressionStatusPrompt.none,
      firstVisit: 0,
      parsed: false,
      defectsCleared: 0,
    },
    status: versionStatusKind.normal,
    analyzeTemplate,
  };
  versionConfig.integrationTest = {
    eventReminder: {
      configConfirmed: false,
      regressionStatusPrompt: regressionStatusPrompt.none,
      firstVisit: 0,
      parsed: false,
    },
    fileRelation: true,
    timeoutTestGen: 10,
    timeoutTestRun: 10,
    coverages: tests.coverageKind.invoke | tests.coverageKind.invokedFunc,
    status: versionStatusKind.normal,
    strictOutputCheck: true,
    autoNullCheck: false,
    dataDepth: 6,
    mcdcKind: tests.mcdcKind.masking,
    stubDepth: invokeRelations.defaultStubDepth,
  };
  const relationVersions = await getRelationVersions(versionId,
    { includeSelf: true });
  for (let i = 0; i < relationVersions.length; i += 1) {
    const relationVersion = relationVersions[i];
    const {
      eventReminder,
      timeoutTestGen,
      timeoutTestRun,
      coverages,
      strictOutputCheck,
      status,
      analyzeTemplate,
      fileRelation,
      testIdentifierRule,
      autoNullCheck,
      dataDepth,
      tolerance,
      mcdcKind,
      stubDepth,
    } = relationVersion;
    const bits = tests.testKind.divideZero | tests.testKind.nullPointer | tests.testKind.outOfBounds | tests.testKind.timeout; // 错误推测相关bit
    switch (relationVersion.versionType) {
      case versionType.unitTest:
        Object.assign(versionConfig.unitTest,
          {
            eventReminder,
            timeoutTestGen,
            timeoutTestRun,
            coverages,
            enableErrorGuess: (coverages & bits) === bits,
            mcdcKind,
            strictOutputCheck,
            status,
            testIdentifierRule: testIdentifierRule || {},
            autoNullCheck,
            dataDepth,
            tolerance: tolerance || null,
          });
        break;
      case versionType.staticAnalyze:
        Object.assign(versionConfig.staticAnalyze, {
          eventReminder,
          analyzeTemplate,
          status,
        });
        break;
      case versionType.integrationTest:
        Object.assign(versionConfig.integrationTest, {
          eventReminder,
          timeoutTestRun,
          timeoutTestGen,
          strictOutputCheck,
          coverages,
          enableErrorGuess: (coverages & bits) === bits,
          mcdcKind,
          fileRelation,
          status,
          testIdentifierRule: testIdentifierRule || {},
          autoNullCheck,
          dataDepth,
          collectCoverageKind: relationVersion.collectCoverageKind || collectCoverageKind.all,
          tolerance: tolerance || null,
          stubDepth,
        });
        break;
      default:
        break;
    }
  }
  return versionConfig;
}
/**
 * 复制回归版本子版本配置
 * @param {String} currentVersionId 当前版本id
 * @param {String} regressionVersionId 回归版本id
 */
async function copyRegressionSubVersionConfig(currentVersionId, regressionVersionId) {
  const regressionConfig = await getVersionConfig(regressionVersionId);
  // 集成测试子版本配置
  const integrationTestConfig = {
    versionType: versionType.integrationTest,
    ...regressionConfig.integrationTest,
  };
  delete integrationTestConfig.eventReminder;
  delete integrationTestConfig.status;
  await addSubVersion(currentVersionId, integrationTestConfig);
  await updateSmartRocketFile(currentVersionId);
  // 静态分析子版本配置
  const staticAnalyzeTestConfig = {
    versionType: versionType.staticAnalyze,
    ...regressionConfig.staticAnalyze,
  };
  delete staticAnalyzeTestConfig.eventReminder;
  delete staticAnalyzeTestConfig.status;
  await addSubVersion(currentVersionId, staticAnalyzeTestConfig);
  await updateSmartRocketFile(currentVersionId);
}
async function getGlobalList(versionId, {
  q,
  functionId,
} = {}) {
  versionId = mongoose.Types.ObjectId(versionId);
  const exec = FileGlobalVariable.aggregate([
    { $match: { versionId } },
    {
      $lookup: {
        from: 'files',
        localField: 'fileId',
        foreignField: '_id',
        as: 'file',
      },
    },
    {
      $unwind: '$file',
    },
  ]);
  exec.append({
    $addFields: {
      globals: {
        $map: {
          input: {
            $objectToArray: '$globals',
          },
          as: 'item',
          in: '$$item.v',
        },
      },
    },
  }, {
    $unwind: '$globals',
  }, {
    $project: {
      _id: 0,
      fileId: '$file._id',
      filePath: '$file.path',
      name: '$globals.@name',
      fullname: '$globals.@fullname',
      mangled: '$globals.@mangled',
      type: '$globals.@type',
      value: '$globals.@value',
      attributes: '$globals.@attributes',
    },
  });
  if (functionId) {
    const funcVariables = await FunctionVariable.findOne({ functionId: mongoose.Types.ObjectId(functionId) });
    if (funcVariables && funcVariables.variables && funcVariables.variables.global) {
      const [staticNames, nonStaticNames] = Object.keys(funcVariables.variables.global).reduce((arr, key) => {
        const [staticNames, nonStaticNames] = arr;
        const g = funcVariables.variables.global[key];
        if (g['@attributes'].find((str) => str === 'isStatic')) {
          staticNames.add(key);
        } else {
          nonStaticNames.add(key);
        }
        return arr;
      }, [new Set(), new Set()]);

      // 排除掉函数自身的globals
      exec.append({
        $match: {
          $or: [
            { fullname: { $nin: [...staticNames, ...nonStaticNames] } },
            { attributes: 'isStatic', fileId: { $ne: funcVariables.fileId } },
          ],
        },
      });
    }
  }
  let arr = await exec.exec();
  if (q) {
    arr = arr.filter((item) => new RegExp(utility.regExpQuote(q)).exec(`${item.filePath}/${item.fullname}`));
  }
  return arr;
}
/**
 * 迁移测试用例
 * @param {Object} sourceProject 源项目
 * @param {Object} sourceVersion 源版本
 * @param {Array[String]} sourceFileIds 需要复制的源文件id
 * @param {Object} creator 创建者
 * @param {String} targetVersionId 目标版本id
 * @param {Object} locale 国际化
 * @param {boolean} isUnitTestToIntegrationTest 是否是移植单测
 * @param {boolean} isRegressionReusedCases 是否回归测试复用用例
 */
async function handleMigrationTestCases(sourceProject, sourceVersion, sourceFileIds, creator, targetVersionId, locale, isUnitTestToIntegrationTest = false, isRegressionReusedCases = false) {
  const exporter = require('./export/test-cases/index').getTestCaseExporter(
    sourceVersion._id,
    'json',
    locale,
    sourceVersion,
    sourceProject,
    creator,
    sourceFileIds,
  );
  const { testsPath, cachePath } = await exporter.exportTestCases();
  await fs.remove(testsPath);
  targetVersionId = mongoose.Types.ObjectId(targetVersionId);
  const targetVersion = await getVersion(targetVersionId);
  const targetProject = await Project.findById(targetVersion.projectId);
  // 测试用例迁移
  await tests.importTests(
    cachePath,
    targetVersion,
    targetProject,
    creator,
    false,
    undefined,
    undefined,
    isUnitTestToIntegrationTest,
    isRegressionReusedCases,
  );
  await fs.remove(cachePath);
}
/**
 * 复制版本测试用例
 * @param {String} sourceVersionId 源版本id
 * @param {Array[String]} functionIds 需要复制的函数id
 * @param {String} targetVersionId 目标版本id
 * @param {Object} creator 创建者(防止跨账户复制)
 * @param {Object} locale 国际化
 * @returns
 */
async function migrateTestCaseOfVersion(sourceVersionId, {
  functionIds = undefined,
  targetVersionId = undefined,
  creator = undefined,
  locale = undefined,
  isUnitTestToIntegrationTest = false, // 是否是移植单测
  isRegressionReusedCases = false, // 是否回归测试复用用例
} = {}) {
  sourceVersionId = mongoose.Types.ObjectId(sourceVersionId);
  const sourceVersion = await getVersion(sourceVersionId);
  const sourceProject = await Project.findById(sourceVersion.projectId);
  // 根据函数来同步
  if (functionIds) {
    const functions = await Function.aggregate([
      {
        $match: {
          _id: { $in: functionIds.map((id) => mongoose.Types.ObjectId(id)) },
          kind: { $in: fileElements.getCountableKinds() },
        },
      },
      {
        $lookup: {
          from: 'files',
          localField: 'fileId',
          foreignField: '_id',
          as: 'file',
        },
      },
      {
        $unwind: '$file',
      },
      {
        $project: {
          _id: 0,
          fileId: '$fileId',
          filePath: '$file.path',
          functionId: '$_id',
          functionName: '$functionName',
          mangledId: '$mangledId',

        },
      },
    ]);
    const sourceTestCases = await Function.aggregate([
      {
        $match: {
          mangledId: { $in: functions.map((func) => func.mangledId) },
          versionId: sourceVersionId,
        },
      },
      {
        $lookup: {
          from: 'files',
          localField: 'fileId',
          foreignField: '_id',
          as: 'file',
        },
      },
      {
        $unwind: '$file',
      },
      {
        $match: {
          'file.path': { $in: functions.map((func) => func.filePath) },
        },
      },
      {
        $lookup: {
          from: 'test-cases',
          localField: '_id',
          foreignField: 'functionId',
          as: 'testCases',
        },
      },
      {
        $unwind: '$testCases',
      },
    ]);
    if (sourceTestCases.length === 0) {
      return { exist: false };
    }
    const sourceFileIds = [];
    sourceTestCases.forEach((sourceTestCase) => {
      const fileId = sourceTestCase.fileId.toString();
      const functionId = sourceTestCase._id.toString();
      let exist = false;
      sourceFileIds.forEach((sourceFileId) => {
        if (sourceFileId.fileId === fileId) {
          sourceFileId.functions.push(functionId);
          exist = true;
        }
      });
      if (!exist) {
        sourceFileIds.push({
          fileId,
          functions: [functionId],
        });
      }
    });
    await handleMigrationTestCases(sourceProject, sourceVersion, sourceFileIds, creator, targetVersionId, locale, isUnitTestToIntegrationTest, isRegressionReusedCases);
  }
  // 根据版本id来同步整个版本用例
  if (!functionIds && targetVersionId) {
    const sourceTestCases = await Test.find({ versionId: sourceVersionId });
    if (sourceTestCases.length === 0) {
      return { exist: false };
    }
    // 只取有用例的函数的文件
    const sourceFileIds = (await Test.aggregate([{
      $match: {
        versionId: sourceVersionId,
      },
    },
    {
      $project: {
        _id: 0,
        fileId: 1,

      },
    },
    {
      $group: {
        _id: '$fileId',
      },
    },
    ]))
      .map((f) => ({ fileId: f._id.toString(), functions: [] }));
    await handleMigrationTestCases(sourceProject, sourceVersion, sourceFileIds, creator, targetVersionId, locale, isUnitTestToIntegrationTest, isRegressionReusedCases);
  }
  return { exist: true };
}
async function integrationTestInvokeGraph(versionId, filesExtname) {
  const graph = {
    functions: [],
    edges: [],
    files: [],
  };
  const rootFile = await File.findOne({
    versionId: mongoose.Types.ObjectId(versionId),
    fileName: '.',
  }, { _id: 1 }).lean();
  graph.files = await File.aggregate([
    {
      $match: {
        versionId: mongoose.Types.ObjectId(versionId),
        fileName: { $nin: ['.', '.smartrocket', '.gitignore'] },
        kind: { $ne: files.kind.fileOutsideProject },
        extname: { $in: [...filesExtname, null] },
      },
    },
    {
      $project: {
        id: '$_id',
        _id: 0,
        name: '$fileName',
        path: '$path',
        parentId: {
          $cond: {
            if: {
              $eq: ['$directoryId', rootFile._id],
            },
            then: null,
            else: '$directoryId',
          },
        },
        kind: '$kind',
      },
    },
  ]);
  graph.functions = await Function.aggregate([
    {
      $match: {
        versionId: mongoose.Types.ObjectId(versionId),
        kind: { $in: fileElements.getCountableKinds() },
        fileId: { $in: graph.files.map((f) => f.id) },
      },
    },
    {
      $project: {
        _id: 1,
        functionName: 1,
        fileId: 1,
      },
    },
    {
      $lookup: {
        from: 'files',
        localField: 'fileId',
        foreignField: '_id',
        as: 'newFuncs',
      },
    },
    {
      $unwind: '$newFuncs',
    },
    {
      $project: {
        _id: 0,
        id: '$_id',
        name: '$functionName',
        fileId: '$fileId',
        filePath: '$newFuncs.path',
      },
    },
  ]);
  const functionIds = graph.functions.map((f) => f.id.toString());
  const invokeFuncsEdgesKeys = [];
  const invokeRelationFuncs = await functionRelations.getRelationsByVersion(versionId);
  invokeRelationFuncs.forEach((func) => {
    // 只获取已定义的函数
    if (!func.toFuncId || !functionIds.includes(func.toFuncId.toString())) {
      return;
    }
    const key = `${func.fromFuncId.toString()}-${func.toFuncId.toString()}`;
    if (!invokeFuncsEdgesKeys.includes(key)) {
      invokeFuncsEdgesKeys.push(key);
      graph.edges.push({ fromFuncId: func.fromFuncId, toFuncId: func.toFuncId });
    }
  });

  return graph;
}
/**
 * 设置收集覆盖率的范围
 * @param {String|ObjectId} versionId 版本id
 * @param {Array[String|ObjectId]} fileIds  选择的文件id
 * @param {Array[String|ObjectId]} functionIds 选择的函数id
 */
async function collectCoverage(versionId, fileIds, functionIds) {
  versionId = mongoose.Types.ObjectId(versionId);
  let collectKind = collectCoverageKind.partial;
  const originalCollectFuncs = await Function.find(
    { versionId, collectCoverage: { $in: [true, null] } },
    { _id: 1, fileId: 1 }
  );
  fileIds = fileIds || [];
  functionIds = functionIds || [];
  // 先重置掉原有的收集覆盖范围
  await Promise.all([
    File.updateMany(
      { versionId },
      { $set: { collectCoverage: false } }
    ),
    Function.updateMany(
      { versionId },
      { $set: { collectCoverage: false } }
    ),
  ]);

  if (fileIds.length > 0) {
    await File.updateMany(
      { versionId, _id: { $in: fileIds.map((id) => mongoose.Types.ObjectId(id)) } },
      { $set: { collectCoverage: true } }
    );
  }
  if (functionIds.length > 0) {
    await Function.updateMany(
      { versionId, _id: { $in: functionIds.map((id) => mongoose.Types.ObjectId(id)) } },
      { $set: { collectCoverage: true } }
    );
  }
  // 收集范围是整个项目
  if (fileIds.length === 0 && functionIds.length === 0) {
    await Promise.all([
      File.updateMany(
        { versionId },
        { $set: { collectCoverage: true } }
      ),
      Function.updateMany(
        { versionId },
        { $set: { collectCoverage: true } }
      ),
    ]);
    functionIds = (await Function.find(
      { versionId, kind: { $in: fileElements.getCountableKinds() } },
      { _id: 1 }
    )).map((func) => func._id.toString());
    collectKind = collectCoverageKind.all;
  }
  await ProjectVersion.updateOne(
    { _id: versionId },
    { $set: { collectCoverageKind: collectKind } }
  );
  // 取出非交集的函数
  const originalCollectFuncsMap = new Map();
  const newCollectFuncsMap = new Map();
  const disableTestFuncIdsMap = new Map();
  const selectedFuncIds = [];
  const unselectedFuncIds = [];
  const disableTestFuncs = await Function.find(
    { versionId, kind: { $nin: fileElements.getCountableKinds() } },
    { _id: 1 }
  );
  disableTestFuncs.forEach((func) => {
    disableTestFuncIdsMap.set(func._id.toString(), {});
  });
  originalCollectFuncs.forEach((func) => {
    originalCollectFuncsMap.set(func._id.toString(), func);
  });
  functionIds.forEach((id) => {
    newCollectFuncsMap.set(id, {});
  });
  // 收集--->不收集
  originalCollectFuncs.forEach((func) => {
    if (!newCollectFuncsMap.has(func._id.toString()) && !disableTestFuncIdsMap.has(func._id.toString())) {
      unselectedFuncIds.push(func._id);
    }
  });
  // 不收集--->收集
  functionIds.forEach((id) => {
    if (!originalCollectFuncsMap.has(id) && !disableTestFuncIdsMap.has(id)) {
      selectedFuncIds.push(mongoose.Types.ObjectId(id));
    }
  });
  // 覆盖率更新
  return functions.onFunctionCoverageSelected(selectedFuncIds, unselectedFuncIds);
}

/**
 * 获取追溯矩阵显示列
 * @param {String|ObjectId} versionId 版本id
 * @param {Array[String]} commonFields 通用显示列
 * @param {Array[String]} userDefinedFields 用户自定义列
 * @returns {Array[Object]} 所有列选中情况
 */
async function getTraceabilityMatrixColumns(versionId,
  {
    commonFields,
    userDefinedFields,
  } = {}) {
  versionId = mongoose.Types.ObjectId(versionId);
  const version = await ProjectVersion.findById(versionId);
  commonFields = commonFields || version.traceabilityMatrix.commonFields;
  userDefinedFields = userDefinedFields || version.traceabilityMatrix.userDefinedFields;
  // console.log(JSON.stringify(version));
  const matrixColumns = [];
  Object.values(traceabilityMatrixCommonColumn).forEach((col) => {
    matrixColumns.push({
      key: col,
      value: commonFields.indexOf(col) >= 0,
    });
  });
  const fields = await UserDefinedFields.find(
    {
      scope: 'testcase',
      domain: version.versionType,
    },
    { _id: 1, name: 1 },
  ).sort({ index: 1 });
  fields.forEach((col) => {
    matrixColumns.push({
      key: col._id,
      name: col.name,
      value: userDefinedFields.indexOf(col._id.toString()) >= 0,
    });
  });
  return matrixColumns;
}

/**
 * 设置追溯矩阵显示列
 * @param {String|ObjectId} versionId 版本id
 * @param {Array[String]} commonFields 通用显示列
 * @param {Array[String]} userDefinedFields 用户自定义列
 * @returns {Array[Object]} 所有列选中情况
 */
async function setTraceabilityMatrixColumns(versionId, commonFields = [], userDefinedFields = []) {
  versionId = mongoose.Types.ObjectId(versionId);
  await ProjectVersion.updateOne(
    { _id: versionId },
    {
      $set: {
        'traceabilityMatrix.commonFields': commonFields,
        'traceabilityMatrix.userDefinedFields': userDefinedFields,
      },
    }
  );
  return getTraceabilityMatrixColumns(versionId, { commonFields, userDefinedFields });
}

/**
 * 获取追溯矩阵测试用例所在页
 * @param {String|ObjectId} versionId 版本id
 * @param {String|ObjectId} testId 测试用例id
 * @param {Number} perPage
 * @returns {Number} 所在页数
 */
async function getTraceabilityMatrixTestCasePage(versionId, testId, perPage) {
  versionId = mongoose.Types.ObjectId(versionId);
  testId = mongoose.Types.ObjectId(testId);
  const test = await Test.findById(testId);
  const CasesLtCurrentTestNumber = await Test.countDocuments(
    {
      versionId,
      $or: [
        { fileId: { $lt: test.fileId } },
        {
          $and: [
            { fileId: { $eq: test.fileId } },
            { functionId: { $lt: test.functionId } },
          ],
        },
        {
          $and: [
            { fileId: { $eq: test.fileId } },
            { functionId: { $eq: test.functionId } },
            { _id: { $lt: testId } },
          ],
        },
      ],
    }
  );
  return parseInt(CasesLtCurrentTestNumber / perPage, 10) + 1;
}

function reverseMap(map) {
  const reversedMap = new Map();
  map.forEach((value, key) => {
    for (let i = 0; i < value.length; i += 1) {
      if (value[i]) {
        if (!reversedMap.has(value[i])) {
          reversedMap.set(value[i], []);
        }
        reversedMap.get(value[i]).push(key);
      }
    }
  });
  return reversedMap;
}

/**
 * 获取追溯矩阵某页测试用例
 * @param {Object} locale 国际化
 * @param {String|ObjectId} versionId 版本id
 * @param {Number} page
 * @param {Number} perPage
 * @returns {Object} 结果数据
 */
async function getTraceabilityMatrixData(locale, versionId, page, perPage) {
  const result = {};
  versionId = mongoose.Types.ObjectId(versionId);
  const version = await ProjectVersion.findById(versionId);
  // get cols from version(filter: in user-defined-fields table)
  const fields = await UserDefinedFields.find(
    {
      scope: 'testcase',
      domain: version.versionType,
    },
    { _id: 1, name: 1 },
  ).sort({ index: 1 });
  const fieldIdtoName = {};
  let sheet2FieldId = '';
  fields.forEach((field) => {
    fieldIdtoName[field._id.toString()] = field.name;
    if (field.name === '用例对应需求编号') {
      sheet2FieldId = field._id;
      result.sheet2ColName = field.name;
    }
  });
  if (sheet2FieldId) {
    result.sheet2Map = new Map();
  }
  const selectedCommonFields = version.traceabilityMatrix.commonFields;
  const selectedDefinedFields = version.traceabilityMatrix.userDefinedFields.filter(
    (id) => Object.keys(fieldIdtoName).indexOf(id) >= 0
  ).map((item) => mongoose.Types.ObjectId(item));
  result.count = await Test.countDocuments({ versionId });
  const retainedFields = { testId: '$_id', _id: 0 };
  let filter = [
    {
      $match: { versionId },
    },
    {
      $sort: {
        fileId: 1,
        functionId: 1,
        _id: 1,
      },
    }];
  if (page !== undefined && perPage !== undefined) {
    filter = filter.concat([{
      $skip: (page - 1) * perPage,
    },
    {
      $limit: perPage,
    }]);
  }
  if (selectedCommonFields.indexOf(traceabilityMatrixCommonColumn.fileName) >= 0
    || selectedCommonFields.indexOf(traceabilityMatrixCommonColumn.testIdentification) >= 0
    || sheet2FieldId) {
    filter = filter.concat([
      {
        $lookup: {
          from: 'files',
          localField: 'fileId',
          foreignField: '_id',
          as: 'file',
        },
      },
      { $unwind: '$file' },
    ]);
    retainedFields.fileName = '$file.fileName';
    retainedFields.fileId = '$fileId';
    retainedFields.filePath = '$file.path';
  }
  if (selectedCommonFields.indexOf(traceabilityMatrixCommonColumn.functionName) >= 0
    || selectedCommonFields.indexOf(traceabilityMatrixCommonColumn.testIdentification) >= 0
    || sheet2FieldId) {
    filter = filter.concat([
      {
        $lookup: {
          from: 'functions',
          localField: 'functionId',
          foreignField: '_id',
          as: 'function',
        },
      },
      { $unwind: '$function' },
    ]);
    retainedFields.functionName = '$function.functionName';
    retainedFields.functionId = '$functionId';
  }
  if (selectedCommonFields.indexOf(traceabilityMatrixCommonColumn.description) >= 0) {
    retainedFields.description = '$description';
    retainedFields.kind = '$kind';
  }
  if (selectedCommonFields.indexOf(traceabilityMatrixCommonColumn.requirementInfo) >= 0) {
    retainedFields.requirementInfo = '$requirementInfo';
  }
  if (selectedCommonFields.indexOf(traceabilityMatrixCommonColumn.testIdentification) >= 0 || sheet2FieldId) {
    retainedFields.identifierNumber = '$identifierNumber';
  }
  // sheet2 need typeNumber Field
  if (selectedCommonFields.indexOf(traceabilityMatrixCommonColumn.typeNumber) >= 0) {
    retainedFields.typeName = '$typeName';
    retainedFields.typeNumber = '$typeNumberString';
  }
  // sheet1 is fields user selected. sheet2 is fields: 用例对应需求编号
  const usedDefinedFields = sheet2FieldId ? selectedDefinedFields.concat([sheet2FieldId]) : selectedDefinedFields;
  if (usedDefinedFields.length > 0) {
    retainedFields.userDefinedFields = {
      $filter: {
        input: '$userDefinedFields',
        as: 'item',
        cond: { $in: ['$$item.key', usedDefinedFields] },
      },
    };
  }
  filter.push({
    $project: retainedFields,
  });
  const testcases = await Test.aggregate(filter);
  result.tests = testcases.map((test) => {
    const data = { testId: test.testId };
    if (selectedCommonFields.indexOf(traceabilityMatrixCommonColumn.fileName) >= 0) {
      data.fileName = test.fileName;
      data.fileId = test.fileId;
    }
    if (selectedCommonFields.indexOf(traceabilityMatrixCommonColumn.functionName) >= 0) {
      data.functionName = test.functionName;
      data.functionId = test.functionId;
    }
    if (selectedCommonFields.indexOf(traceabilityMatrixCommonColumn.testIdentification) >= 0) {
      data.testIdentification = tests.getTestIdentifier({
        testIdentifierRule: version.testIdentifierRule,
        functionName: test.functionName,
        filepath: test.filePath,
        identifierNumber: test.identifierNumber,
      });
    }
    if (selectedCommonFields.indexOf(traceabilityMatrixCommonColumn.description) >= 0) {
      data.description = test.description || '';
      if (test.kind !== tests.testKind.modified && test.kind) {
        data.description = locale.__('tests.structuralCoverageCase'); // eslint-disable-line no-underscore-dangle
      }
    }
    if (selectedCommonFields.indexOf(traceabilityMatrixCommonColumn.requirementInfo) >= 0) {
      data.requirementInfo = test.requirementInfo || '';
    }
    if (selectedCommonFields.indexOf(traceabilityMatrixCommonColumn.typeNumber) >= 0) {
      data.typeName = test.typeName;
      data.typeNumber = test.typeNumber;
    }
    data.userDefinedFields = {};
    const idtoValue = {};
    if (test.userDefinedFields) {
      test.userDefinedFields.forEach((field) => {
        idtoValue[field.key] = field.value;
      });
    }
    selectedDefinedFields.forEach((id) => {
      data.userDefinedFields[id.toString()] = idtoValue[id] || '';
    });
    if (sheet2FieldId && idtoValue[sheet2FieldId.toString()] && test.identifierNumber) {
      result.sheet2Map.set(tests.getTestIdentifier({
        testIdentifierRule: version.testIdentifierRule,
        functionName: test.functionName,
        filepath: test.filePath,
        identifierNumber: test.identifierNumber,
      }), idtoValue[sheet2FieldId.toString()].split(','));
    }
    return data;
  });
  result.commonFields = selectedCommonFields;
  result.userDefinedFields = selectedDefinedFields.map((id) => ({
    key: id.toString(),
    name: fieldIdtoName[id.toString()],
  }));
  if (sheet2FieldId) {
    result.requirementNumberToCase = reverseMap(result.sheet2Map);
    delete result.sheet2Map;
  }
  return result;
}

/**
 * 计算一到两位的xlsx文件中列标识增加
 * @param {String} col
 * @returns {String}
 */
function getNextXlsxColumn(col) {
  if (!col) {
    return 'A';
  }
  if (col.slice(-1) !== 'Z') {
    const strs = col.split('');
    strs[strs.length - 1] = String.fromCharCode(strs[strs.length - 1].charCodeAt() + 1);
    return strs.join('');
  }
  if (col.length === 1) {
    return 'AA';
  }
  const strs = col.split('');
  strs[1] = 'A';
  strs[0] = String.fromCharCode(strs[0].charCodeAt() + 1);
  return strs.join('');
}

/**
 * 获取追溯矩阵下载文件
 * @param {String|ObjectId} versionId 版本id
 * @param {String} excelName
 * @param {Object} locale
 * @returns {Array} 结果路径
 */
async function getTraceabilityMatrixXlsx(versionId, excelName, locale) {
  const testsData = await getTraceabilityMatrixData(locale, versionId);

  // excel cache path
  const outputDir = path.join(cacheUtil.getConfigPath('exportDir'), randomstring.generate(16));
  await fs.ensureDir(outputDir);
  const outPath = path.join(outputDir, excelName);
  const fileExcel = new Excel();
  fileExcel.addSheet('Sheet1');

  // build data
  const columnMarks = [''];
  const columns = [];
  testsData.commonFields.forEach((field) => {
    columnMarks.push(getNextXlsxColumn(columnMarks[columnMarks.length - 1]));
    columns.push({
      header: locale.__(`traceAbilityMatrix.${field}`),
      key: field,
      width: 16.93,
    });
  });
  testsData.userDefinedFields.forEach((field) => {
    columnMarks.push(getNextXlsxColumn(columnMarks[columnMarks.length - 1]));
    columns.push({
      header: field.name,
      key: `user-defind-field${field.key}`,
      width: 16.93,
    });
  });
  const data = [];
  testsData.tests.forEach((item) => {
    const testData = {};
    testsData.commonFields.forEach((field) => {
      testData[field] = item[field];
    });
    testsData.userDefinedFields.forEach((field) => {
      testData[`user-defind-field${field.key}`] = item.userDefinedFields[field.key];
    });
    data.push(testData);
  });
  fileExcel.addRows(columns, data);

  // remove '' (which helps for increse column marks) from markList
  columnMarks.shift();
  columnMarks.forEach((column) => {
    for (let i = 1; i <= testsData.count + 1; i += 1) {
      fileExcel.setCell(column.concat(String(i)));
    }
  });

  if (testsData.requirementNumberToCase) {
    fileExcel.addSheet('LLR-TC');
    const sheet2Data = [];
    testsData.requirementNumberToCase.forEach((value, key) => {
      sheet2Data.push({
        case: value.join(','),
        requirementNumber: key,
      });
    });
    fileExcel.addRows([
      {
        header: testsData.sheet2ColName,
        key: 'requirementNumber',
        width: 16.93,
      },
      {
        header: locale.__('traceAbilityMatrix.testIdentification'),
        key: 'case',
        width: 16.93,
      },
    ], sheet2Data);
    ['A', 'B'].forEach((column) => {
      for (let i = 1; i <= sheet2Data.length + 1; i += 1) {
        fileExcel.setCell(column.concat(String(i)));
      }
    });
  }

  await fileExcel.writeFile(outPath);
  return [outPath, outputDir];
}
eventHub.on(eventKind.EVENT_PROJECT_REMOVE, async (projectId) => {
  projectId = mongoose.Types.ObjectId(projectId);
  const versionIds = (await ProjectVersion.find({ projectId }, ['_id'])).map((version) => version._id.toString());
  await Promise.all(versionIds.map(
    (versionId) => eventHub.emit(eventKind.EVENT_VERSION_REMOVE, versionId)
  ));
  return ProjectVersion.deleteMany({ projectId });
});

/**
 * Export model definition object.
 */
module.exports = {
  addVersion,
  addSubVersion,
  addRegressionVersion,

  collectCoverage,
  complementRootDirectory,
  copyRegressionSubVersionConfig,

  diffProjectVersions,

  filter,
  fileCountInfo,
  functionCoverageDistribution,

  getJson,
  getCount,
  getVersion,
  getVersions,
  getOverview,
  getGlobalList,
  getOverviewAll,
  getSummaryJson,
  getVersionConfig,
  getVersionOptions,
  getRelationVersions,
  getLastCreatedVersion,
  getDefaultProjectType,
  getProjectVersionStatistics,
  getStaticAnalyzeOverviewList,
  getSmartRocketFormatContent,

  initSubVersion,
  integrationTestInvokeGraph,

  namesSet,

  migrateTestCaseOfVersion,

  removeVersion,
  regressionStatusPrompt,

  updateVersion,
  updateVisitTime,
  updateVersionFile,
  updateSmartRocketFile,
  updateEnvironmentChanged,
  updateRegressionStatusPrompt,

  statusKind: versionStatusKind,
  systemType,

  testResultOverview,

  versionType,
  versionConfigFields,
  collectCoverageKind,

  traceabilityMatrixCommonColumn,
  setTraceabilityMatrixColumns,
  getTraceabilityMatrixColumns,
  getTraceabilityMatrixTestCasePage,
  getTraceabilityMatrixData,
  getTraceabilityMatrixXlsx,
};
exports = Object.assign(exports, module.exports);
